├── dummy
├── assets
│ └── favicon.ico
├── app.yaml
├── api.js
├── module
│ ├── package.json
│ ├── index.js
│ └── package-lock.json
├── package.json
├── .gcloudignore
├── index.html
└── server.js
├── server
├── twilio
│ ├── twilio.js
│ └── email.js
├── models
│ ├── postgres-init.sql
│ ├── postgres-client.js
│ └── influx-client.js
├── routes
│ ├── logRouter.js
│ └── chartdata.js
├── controllers
│ ├── pgController.js
│ └── influxController.js
└── server.js
├── .gitignore
├── examples
├── intro.png
├── home_view.png
├── endpoint_view.png
├── monitoring_view.png
└── simulation_view.png
├── assets
├── datadoc_logo.png
├── node-logo-color.png
├── npm-logo-color.png
├── chartjs-logo-color.png
├── docker-logo-color.png
├── express-logo-color.png
├── react-logo-color.png
├── twilio-logo-color.png
├── webpack-logo-color.png
├── electron-logo-color.png
├── influxdb-logo-color.png
├── postgres-logo-color.png
└── material-ui-logo-color.png
├── client
├── styles
│ ├── Header.scss
│ ├── WorkspaceBox.scss
│ ├── NavBar.scss
│ ├── Charts.scss
│ ├── globals.scss
│ ├── Settings.scss
│ └── AddWorkspace.scss
├── components
│ ├── Forward.jsx
│ ├── FlashError.jsx
│ ├── HomeButton.jsx
│ ├── NavButtons.jsx
│ ├── SearchBar.jsx
│ ├── WorkspaceCard.jsx
│ ├── LogTable.jsx
│ ├── URI.jsx
│ ├── Histogram.jsx
│ ├── LineChart.jsx
│ ├── DonutChart.jsx
│ ├── Settings.jsx
│ ├── Home.jsx
│ ├── WorkspaceInfo.jsx
│ └── URITable.jsx
├── index.js
├── containers
│ ├── Production.jsx
│ ├── Header.jsx
│ ├── Dashboard.jsx
│ ├── Topbar.jsx
│ ├── DrawerContents.jsx
│ ├── ChartsContainer.jsx
│ ├── Sidebar.jsx
│ ├── SimulationView.jsx
│ ├── WorkspaceView.jsx
│ ├── App.jsx
│ └── NavBar.jsx
└── theme.js
├── .env.example
├── template.html
├── docker-compose.yml
├── LICENSE
├── electron
└── electron-main.js
├── webpack.config.js
├── package.json
└── README.md
/dummy/assets/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dummy/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs16
--------------------------------------------------------------------------------
/server/twilio/twilio.js:
--------------------------------------------------------------------------------
1 | const twilio = require("twilio");
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | dist
4 | .DS_Store
5 | *.env
6 |
--------------------------------------------------------------------------------
/examples/intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/examples/intro.png
--------------------------------------------------------------------------------
/assets/datadoc_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/datadoc_logo.png
--------------------------------------------------------------------------------
/examples/home_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/examples/home_view.png
--------------------------------------------------------------------------------
/assets/node-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/node-logo-color.png
--------------------------------------------------------------------------------
/assets/npm-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/npm-logo-color.png
--------------------------------------------------------------------------------
/examples/endpoint_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/examples/endpoint_view.png
--------------------------------------------------------------------------------
/assets/chartjs-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/chartjs-logo-color.png
--------------------------------------------------------------------------------
/assets/docker-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/docker-logo-color.png
--------------------------------------------------------------------------------
/assets/express-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/express-logo-color.png
--------------------------------------------------------------------------------
/assets/react-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/react-logo-color.png
--------------------------------------------------------------------------------
/assets/twilio-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/twilio-logo-color.png
--------------------------------------------------------------------------------
/assets/webpack-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/webpack-logo-color.png
--------------------------------------------------------------------------------
/examples/monitoring_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/examples/monitoring_view.png
--------------------------------------------------------------------------------
/examples/simulation_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/examples/simulation_view.png
--------------------------------------------------------------------------------
/assets/electron-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/electron-logo-color.png
--------------------------------------------------------------------------------
/assets/influxdb-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/influxdb-logo-color.png
--------------------------------------------------------------------------------
/assets/postgres-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/postgres-logo-color.png
--------------------------------------------------------------------------------
/assets/material-ui-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DataDoc/HEAD/assets/material-ui-logo-color.png
--------------------------------------------------------------------------------
/client/styles/Header.scss:
--------------------------------------------------------------------------------
1 | .header-button{
2 | opacity: 0.5;
3 | width: 100px;
4 | height: 30px;
5 | border-radius: 3px;
6 | cursor: auto;
7 | // text-align: right;
8 | float: right;
9 | }
--------------------------------------------------------------------------------
/client/styles/WorkspaceBox.scss:
--------------------------------------------------------------------------------
1 | // .workspaceBox{
2 | // padding: 5px;
3 | // width: 30%;
4 | // margin: 5px 5px 13px 10px;
5 | // float: left;
6 | // font-size: .8em;
7 | // box-shadow: 5px 5px 15px rgba(0, 0, 0, .2);
8 | // color: brown
9 | // }
--------------------------------------------------------------------------------
/dummy/api.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 |
3 | router.get('/', (req, res) => {
4 | res.send(`you're using the api`);
5 | });
6 |
7 | router.get('/:text', (req, res) => {
8 | res.send(req.params.text.split('').reverse().join(''));
9 | });
10 |
11 | module.exports = router;
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=
2 | PORT=
3 | SERVER_URL=
4 | PG_HOST=
5 | PG_PORT=
6 | PG_USER=
7 | PG_PASS=
8 | PG_DB=
9 | DB_INFLUXDB_INIT_MODE=
10 | DB_INFLUXDB_INIT_USERNAME=
11 | DB_INFLUXDB_INIT_PASSWORD=
12 | DB_INFLUXDB_INIT_ORG=
13 | DB_INFLUXDB_INIT_BUCKET=
14 | DB_INFLUXDB_INIT_RETENTION=
15 | DB_INFLUXDB_INIT_ADMIN_TOKEN=
16 |
--------------------------------------------------------------------------------
/client/components/Forward.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from "react-router-dom";
3 |
4 | const Forward = () => {
5 | let navigate = useNavigate();
6 | return (
7 | <>
8 |
9 | >
10 | );
11 | };
12 |
13 | export default Forward;
14 |
--------------------------------------------------------------------------------
/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DataDoc
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/components/FlashError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FlashError = (props) => {
4 | const { errorMessage } = props;
5 | console.log(errorMessage)
6 | return (
7 |
10 | );
11 | };
12 | export default FlashError;
13 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./containers/App.jsx";
4 | import { HashRouter } from "react-router-dom";
5 |
6 | const container = document.getElementById("root");
7 | const root = createRoot(container);
8 |
9 | root.render(
10 | <>
11 |
12 | >
13 | );
--------------------------------------------------------------------------------
/dummy/module/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-endpoints-monitor",
3 | "version": "1.0.2",
4 | "description": "",
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "ISC",
9 | "dependencies": {
10 | "express": "^4.18.2",
11 | "express-list-endpoints": "^6.0.0",
12 | "response-time": "^2.3.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/dummy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node ."
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "express-endpoints-monitor": "^1.0.2",
14 | "nodemon": "^2.0.20"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/containers/Production.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | //import components here
4 | import URIList from '../components/URIList.jsx'
5 | import ChartsContainer from "./ChartsContainer.jsx";
6 | import NavBar from "./NavBar.jsx";
7 |
8 | const Production = () => {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default Production;
--------------------------------------------------------------------------------
/server/models/postgres-init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE workspaces (
2 | _id SERIAL,
3 | name TEXT NOT NULL,
4 | domain TEXT NOT NULL,
5 | port INTEGER,
6 | metrics_port INTEGER
7 | );
8 |
9 | CREATE TABLE endpoints (
10 | _id SERIAL,
11 | method TEXT NOT NULL,
12 | path TEXT NOT NULL,
13 | tracking BOOLEAN DEFAULT false,
14 | workspace_id INTEGER NOT NULL
15 | );
16 |
17 | ALTER TABLE endpoints
18 | ADD CONSTRAINT endpoints_uq
19 | UNIQUE (method, path, workspace_id);
--------------------------------------------------------------------------------
/client/components/HomeButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MemoryRouter as Router } from "react-router-dom";
3 |
4 | // import Production from '../containers/Production'
5 |
6 | const HomeButton = (props) => {
7 |
8 | const { onClick } = props;
9 |
10 | return (
11 |
16 | );
17 | };
18 |
19 | export default HomeButton;
20 |
--------------------------------------------------------------------------------
/server/routes/logRouter.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const compression = require("compression");
3 | const influxController = require("../controllers/influxController.js");
4 |
5 | const router = express.Router();
6 | router.use(compression());
7 |
8 | // get line chart data
9 | router.get(
10 | "/",
11 | influxController.getEndpointLogs,
12 | (req, res) => {
13 | return res.status(200).json(res.locals.logs);
14 | }
15 | );
16 |
17 | module.exports = router;
18 |
--------------------------------------------------------------------------------
/dummy/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 | # If you would like to upload your .git directory, .gitignore file or files
11 | # from your .gitignore file, remove the corresponding line
12 | # below:
13 | .git
14 | .gitignore
15 |
16 | # Node.js dependencies:
17 | node_modules/
18 |
19 | module/
--------------------------------------------------------------------------------
/server/models/postgres-client.js:
--------------------------------------------------------------------------------
1 | const { Pool, Client } = require("pg");
2 | const dotenv = require("dotenv");
3 | dotenv.config();
4 |
5 | const { PG_HOST, PG_PORT, PG_USER, PG_PASS, PG_DB } = process.env;
6 |
7 | const client = new Client({
8 | host: PG_HOST,
9 | port: PG_PORT,
10 | user: PG_USER,
11 | password: PG_PASS,
12 | database: PG_DB,
13 | })
14 |
15 | client.connect().then(() => {
16 | console.log("Connected to database");
17 | client.query("SELECT NOW()");
18 | })
19 |
20 | module.exports = {
21 | query: (text, params, callback) => {
22 | return client.query(text, params, callback);
23 | },
24 | };
--------------------------------------------------------------------------------
/client/components/NavButtons.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from "react-router-dom";
3 | import { IconButton } from "@mui/material";
4 | import { ArrowBack, ArrowForward } from '@mui/icons-material';
5 |
6 | export const Back = () => {
7 | let navigate = useNavigate();
8 | return (
9 | navigate(-1)}>
10 |
11 |
12 | );
13 | };
14 |
15 | export const Forward = () => {
16 | let navigate = useNavigate();
17 | return (
18 | navigate(+1)}>
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/client/styles/NavBar.scss:
--------------------------------------------------------------------------------
1 | .navbar-container{
2 | height: 100% !important;
3 | display: flex;
4 | flex-direction: column;
5 | // border-right: 1px solid;
6 | border-radius: 0;
7 | // border-color: rgba(132, 134, 133, 0.693);
8 | // background-color: rgb(255, 255, 255);
9 | transition: 0.5s ease;
10 | position: fixed;
11 | }
12 |
13 | .navbar-button{
14 | height: 50px;
15 | border-top-right-radius: 10rem;
16 | border-bottom-right-radius: 9rem;
17 | width: 10px;
18 | position: absolute;
19 | outline: none;
20 | z-index: 1;
21 | background-color: rgb(197, 181, 181);
22 | border-color: rgba(174, 200, 188, 0.693);
23 | border-left: 0;
24 | cursor: pointer;
25 | }
26 |
--------------------------------------------------------------------------------
/client/styles/Charts.scss:
--------------------------------------------------------------------------------
1 | // .charts-container {
2 | // display: grid;
3 | // width: 100vw;
4 | // grid-template-areas:
5 | // "3fr 2fr"
6 | // "3fr 2fr";
7 | // gap: 2px;
8 | // // box-sizing: border-box;
9 | // }
10 |
11 | // // .chartWrapper {
12 | // // position: relative;
13 | // // }
14 |
15 | // .chartWrapper > canvas {
16 | // position: absolute;
17 | // left: 0;
18 | // top: 0;
19 | // pointer-events: none;
20 | // // width: 100%;
21 | // }
22 |
23 | // .chartAreaWrapper {
24 | // width: 60vw;
25 | // overflow-x: scroll;
26 | // }
27 |
28 | // .line-chart {
29 | // width: 80vw;
30 | // height: 40vh;
31 | // > canvas {
32 | // cursor: crosshair;
33 | // }
34 | // }
35 |
--------------------------------------------------------------------------------
/server/routes/chartdata.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const compression = require("compression");
3 | const influxController = require("../controllers/influxController.js");
4 |
5 | const router = express.Router();
6 | router.use(compression());
7 |
8 | // * Retrieve line chart data from InfluxDB
9 | router.get(
10 | "/",
11 | influxController.getRespTimeLineData,
12 | influxController.getRespTimeHistData,
13 | influxController.getReqFreqLineData,
14 | influxController.getStatusPieData,
15 | (req, res) => {
16 | return res.status(200).json(res.locals.data);
17 | }
18 | );
19 |
20 | // * Update chart range
21 | router.post("/",
22 | influxController.updateRange,
23 | (req, res) => res.sendStatus(204)
24 | )
25 |
26 | module.exports = router;
27 |
--------------------------------------------------------------------------------
/client/containers/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import '../styles/Header.scss'
3 |
4 | //header to create links that will be used to navigate between routes
5 | const Header = (props) => {
6 | const { monitoring, simulation } = props
7 | return(
8 |
22 | )
23 | }
24 |
25 | export default Header
--------------------------------------------------------------------------------
/client/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap");
2 |
3 | // * {
4 | // transition: background 0.2s, color 0.2s;
5 | // }
6 |
7 | body {
8 | top: 0;
9 | left: 0;
10 | margin: 0;
11 | width: 100vw;
12 | box-sizing: border-box;
13 | -moz-box-sizing: border-box;
14 | -webkit-box-sizing: border-box;
15 | }
16 |
17 | .content {
18 | box-sizing: border-box;
19 | -moz-box-sizing: border-box;
20 | -webkit-box-sizing: border-box;
21 | }
22 |
23 | .fullapp,
24 | .content {
25 | height: 100%;
26 | width: 100%;
27 | font-family: "Source Sans Pro", sans-serif;
28 | }
29 |
30 | .fullapp {
31 | display: flex;
32 | position: relative;
33 | }
34 |
35 | * {
36 | -moz-box-sizing: border-box;
37 | -webkit-box-sizing: border-box;
38 | box-sizing: border-box;
39 | }
40 |
--------------------------------------------------------------------------------
/client/styles/Settings.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | left: 0;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | background-color: rgba(128,128,128,0.8);
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
13 | .modal-content {
14 | padding: 40px;
15 | width: 25vw;
16 | height: 50vh;
17 | background-color: white;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: space-evenly;
21 | border-radius: 20px;
22 | }
23 |
24 | .close-button {
25 | align-self: flex-end;
26 | width: 40px;
27 | border-radius: 20px;
28 | }
29 |
30 | .modal-header {
31 | color: black;
32 | }
33 |
34 | .workspaceDomain {
35 | height: 8%;
36 | border-radius: 10px;
37 | }
38 |
39 | .workspacePort {
40 | height: 8%;
41 | border-radius: 10px;
42 | }
43 |
44 | .submit-button {
45 | height: 8%;
46 | border-radius: 10px;
47 | }
--------------------------------------------------------------------------------
/client/components/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Input, TextField } from "@mui/material"
3 | import { Search } from "@mui/icons-material"
4 |
5 | const SearchBar = (props) => {
6 |
7 | const { handleSearchChange } = props;
8 |
9 | return (
10 | <>
11 |
28 | >
29 | );
30 | };
31 |
32 | export default SearchBar;
33 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | influxdb:
4 | image: influxdb:2.4
5 | ports:
6 | - '8086:8086'
7 | environment:
8 | - DOCKER_INFLUXDB_INIT_MODE=${DB_INFLUXDB_INIT_MODE}
9 | - DOCKER_INFLUXDB_INIT_USERNAME=${DB_INFLUXDB_INIT_USERNAME}
10 | - DOCKER_INFLUXDB_INIT_PASSWORD=${DB_INFLUXDB_INIT_PASSWORD}
11 | - DOCKER_INFLUXDB_INIT_ORG=${DB_INFLUXDB_INIT_ORG}
12 | - DOCKER_INFLUXDB_INIT_BUCKET=${DB_INFLUXDB_INIT_BUCKET}
13 | - DOCKER_INFLUXDB_INIT_RETENTION=${DB_INFLUXDB_INIT_RETENTION}
14 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${DB_INFLUXDB_INIT_ADMIN_TOKEN}
15 |
16 | postgres:
17 | image: postgres:14.1-alpine
18 | ports:
19 | - '5433:5432'
20 | restart: always
21 | environment:
22 | - POSTGRES_USER=postgres
23 | - POSTGRES_PASSWORD=postgres
24 | volumes:
25 | - db:/var/lib/postgresql/data
26 | - ./server/models/postgres-init.sql:/docker-entrypoint-initdb.d/init.sql
27 | volumes:
28 | db:
29 | driver: local
--------------------------------------------------------------------------------
/client/styles/AddWorkspace.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | left: 0;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | background-color: rgba(128,128,128,0.8);
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
13 | .modal-content {
14 | padding: 40px;
15 | width: 25vw;
16 | height: 50vh;
17 | background-color: white;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: space-evenly;
21 | border-radius: 20px;
22 | }
23 |
24 | .close-button {
25 | align-self: flex-end;
26 | width: 40px;
27 | border-radius: 20px;
28 | }
29 |
30 | .modal-header {
31 | color: black;
32 | }
33 |
34 | .workspaceName {
35 | height: 8%;
36 | border-radius: 10px;
37 | }
38 |
39 | .alert-status-codes {
40 | height: 100%;
41 | resize: none;
42 | border-radius: 10px;
43 | }
44 |
45 | .monitoring-frequency{
46 | height: 40%;
47 | resize: none;
48 | border-radius: 10px;
49 | }
50 |
51 | .submit-button {
52 | height: 8%;
53 | border-radius: 10px;
54 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 DataDoc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/electron/electron-main.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const { app, BrowserWindow } = require("electron");
3 | const url = require("url");
4 |
5 | const { SERVER_URL } = process.env;
6 |
7 | function createWindow() {
8 | let win = new BrowserWindow({
9 | width: 960,
10 | height: 700,
11 | webPreferences: {
12 | nodeIntegration: true,
13 | worldSafeExecuteJavaScript: true,
14 | contextIsolation: true,
15 | },
16 | });
17 | if (process.env.NODE_ENV === 'production') {
18 | indexPath = url.format({
19 | protocol: 'http:',
20 | host: 'localhost:9990',
21 | pathname: 'index.html',
22 | slashes: true
23 | })
24 | } else {
25 | indexPath = url.format({
26 | protocol: 'http:',
27 | host: 'localhost:8080',
28 | pathname: 'index.html',
29 | slashes: true
30 | })
31 | }
32 | setTimeout(() => win.loadURL(indexPath), 1000);
33 | win.once('ready-to-show', () => {
34 | win.show()
35 | })
36 | }
37 |
38 | app.whenReady().then(() => {
39 | createWindow();
40 | app.on("activate", () => {
41 | if (BrowserWindow.getAllWindows().length === 0) createWindow();
42 | });
43 | });
44 |
45 | app.on("window-all-closed", () => {
46 | if (process.platform !== "darwin") app.quit();
47 | });
48 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 | const Dotenv = require("dotenv-webpack");
3 | const path = require("path");
4 |
5 | module.exports = {
6 | mode: process.env.NODE_ENV || "production",
7 | entry: path.resolve(__dirname, "/client/index.js"),
8 | output: {
9 | path: path.resolve(__dirname, "dist"),
10 | filename: "bundle.js",
11 | publicPath: "/",
12 | },
13 | devtool: "source-map",
14 | devServer: {
15 | static: {
16 | directory: path.resolve(__dirname, "dist"),
17 | },
18 | port: 8080,
19 | hot: true,
20 | compress: true,
21 | historyApiFallback: true,
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.jsx?/,
27 | exclude: /node_modules/,
28 | loader: "babel-loader",
29 | options: { presets: ["@babel/env", "@babel/preset-react"] },
30 | },
31 | {
32 | test: /.(css|scss)$/,
33 | exclude: /node_modules/,
34 | use: ["style-loader", "css-loader", "sass-loader"],
35 | },
36 | ],
37 | },
38 | plugins: [
39 | new HtmlWebpackPlugin({
40 | filename: "index.html",
41 | template: "./template.html",
42 | }),
43 | new Dotenv({
44 | systemvars: true,
45 | }),
46 | ],
47 | };
48 |
--------------------------------------------------------------------------------
/dummy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | JONATHAN'S WEBSITE
8 |
30 |
31 |
32 | Welcome to jonathan.org
33 |
34 |
35 | fast
36 | 🐆
37 |
38 |
39 |
40 | slow
41 | 🐌
42 |
43 |
44 |
45 | new link
46 |
47 |
48 |
49 |
50 | Use our API!
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/server/twilio/email.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv')
2 | const path = require('path');
3 | dotenv.config({path: path.resolve(__dirname, "../../.env")});
4 |
5 | const sendGridMail = require("@sendgrid/mail");
6 | // console.log(process.env.SENDGRID_API_KEY)
7 | sendGridMail.setApiKey(process.env.SENDGRID_API_KEY);
8 |
9 | //function to build out the body of the email
10 | const msg = () => {
11 | const body =
12 | "This is a notifaction from the Datatective team. We have found an outage in your system, please open up the Datatective desktop application for more information.";
13 | return {
14 | to: "example@domain.com", // ! query from database
15 | from: "datadocteam@gmail.com",
16 | subject: "IMPORTANT: outage detected from datatective",
17 | text: body,
18 | html: "{body}",
19 | };
20 | };
21 |
22 | //send the message using the send method from the SendGrid email package
23 | async function sendEmail() {
24 | try {
25 | await sendGridMail.send(msg());
26 | console.log("Email notification successfully send");
27 | } catch (error) {
28 | console.log("There was an error sending an email notification");
29 | console.log("THIS IS THE ERROR: ", error);
30 | console.log("this is the API key: ", process.env.SENDGRID_API_KEY);
31 | if (error.response) {
32 | console.log(error.response.body);
33 | }
34 | }
35 | }
36 |
37 | (async () => {
38 | console.log("Sending email");
39 | await sendEmail();
40 | })();
41 |
--------------------------------------------------------------------------------
/dummy/server.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const express = require("express");
3 | const app = express();
4 | const apiRouter = require("./api.js");
5 | const ourModule = require("express-endpoints-monitor");
6 |
7 | const PORT = 3000;
8 | const METRICS_PORT = 9991;
9 |
10 | app.use(express.json());
11 | app.use(ourModule.gatherMetrics);
12 |
13 | app.use(express.static(path.join(__dirname, "assets")));
14 |
15 | app.get("/", (req, res) =>
16 | res.sendFile(path.resolve(__dirname, "./index.html"))
17 | );
18 |
19 | app.use("/api", apiRouter);
20 |
21 | app.get("/fast",
22 | ourModule.registerEndpoint,
23 | (req, res) => {
24 | res.status(201).send("fast");
25 | });
26 |
27 | app.put("/fast",
28 | ourModule.registerEndpoint,
29 | (req, res) => {
30 | res.status(204).send("fast");
31 | });
32 |
33 | app.get("/slow", ourModule.registerEndpoint, (req, res) => {
34 | const validStatusCodes = [
35 | 100, 102, 200, 200, 200, 202, 203, 204, 204, 210, 301, 302, 400, 401, 403, 404, 500, 505
36 | ];
37 |
38 | const statusCode =
39 | validStatusCodes[Math.floor(Math.random() * validStatusCodes.length)];
40 | const artificialDelay = Math.random() * 900;
41 | setTimeout(() => res.status(statusCode).send("slow"), artificialDelay);
42 | });
43 |
44 | app.listen(PORT, () => {
45 | console.log(`Target server started on port ${PORT}`);
46 |
47 | // ourModule.exportEndpoints(app);
48 | ourModule.exportAllEndpoints(app);
49 | ourModule.startMetricsServer(METRICS_PORT);
50 | });
51 |
--------------------------------------------------------------------------------
/client/components/WorkspaceCard.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Typography } from "@mui/material";
2 | import { useTheme } from "@mui/material/styles";
3 | import React from "react";
4 | import { useNavigate } from "react-router-dom";
5 | import "../styles/WorkspaceBox.scss";
6 |
7 | const WorkspaceCard = (props) => {
8 | const { workspaceId, name, domain, port, metricsPort, deleteWorkspace } = props;
9 | const theme = useTheme();
10 | const navigate = useNavigate();
11 | return (
12 | <>
13 |
14 |
{
16 | navigate(`/workspace/${workspaceId}`, {
17 | state: {
18 | workspaceId,
19 | name,
20 | domain,
21 | port,
22 | metricsPort,
23 | }
24 | })}}
25 | >
26 |
30 | {name}
31 |
32 |
35 | Domain: {domain}
36 |
37 |
41 | Port: {port || "N/A"}
42 |
43 |
44 |
45 |
52 |
53 |
54 | >
55 | );
56 | };
57 |
58 | export default WorkspaceCard;
59 |
--------------------------------------------------------------------------------
/client/components/LogTable.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const { SERVER_URL } = process.env;
4 |
5 | const LogTable = (props) => {
6 | const { method, path, isMonitoring } = props;
7 |
8 | let [logEntries, setLogEntries] = useState([]);
9 |
10 | if (isMonitoring) {
11 | setTimeout(async () => {
12 | const encodedPath = path.replaceAll("/", "%2F");
13 | setLogEntries(
14 | (
15 | await (
16 | await fetch(
17 | `${SERVER_URL}/logdata/?method=${method}&path=${encodedPath}`
18 | )
19 | ).json()
20 | ).map((log) => {
21 | return (
22 |
30 | );
31 | })
32 | );
33 | }, 2000);
34 | }
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 | | Time |
42 | Path |
43 | Method |
44 | Response Time |
45 | Status Code |
46 |
47 |
48 | {logEntries}
49 |
50 | >
51 | );
52 | };
53 |
54 | const LogEntry = (props) => {
55 | const { timestamp, path, method, res_time, status_code } = props;
56 |
57 | return (
58 |
59 | | {timestamp} |
60 | {path} |
61 | {method} |
62 | {res_time} |
63 | {status_code} |
64 |
65 | );
66 | };
67 |
68 | export default LogTable;
69 |
--------------------------------------------------------------------------------
/server/controllers/pgController.js:
--------------------------------------------------------------------------------
1 | const postgresClient = require("../models/postgres-client");
2 | const influxController = require("./influxController");
3 |
4 | const pgController = {
5 |
6 | deleteEndpointsByWorkspaceId: async (req, res, next) => {
7 | // const {workspaceId} = req.params;
8 | const {workspaceId} = res.locals;
9 | const queryText = `
10 | DELETE
11 | FROM endpoints
12 | WHERE workspace_id=${workspaceId}
13 | ;`;
14 | try {
15 | await postgresClient.query(queryText);
16 | } catch (err) {
17 | return next(err);
18 | }
19 | return next();
20 | },
21 |
22 | updateEndpointById: async (req, res, next) => {
23 | const _id = Number(req.params?._id);
24 | const { method, path, tracking } = req.body;
25 | const queryText = `
26 | UPDATE
27 | endpoints
28 | SET
29 | method='${method}',
30 | path='${path}',
31 | tracking=${tracking}
32 | WHERE
33 | _id=${_id} ;
34 | `;
35 | try {
36 | postgresClient.query(queryText);
37 | return next();
38 | } catch (err) {
39 | return next(err);
40 | }
41 | },
42 | updateEndpointByRoute: async (req, res, next) => {
43 | const { workspaceId, method, path, tracking} = req.body
44 | const queryText = `
45 | UPDATE
46 | endpoints
47 | SET
48 | method='${method}',
49 | path='${path}',
50 | tracking=${tracking}
51 | WHERE
52 | workspace_id=${workspaceId} AND
53 | method='${method}' AND
54 | path='${path}'
55 | ;`;
56 | try {
57 | postgresClient.query(queryText);
58 | } catch (err) {
59 | return next(err);
60 | }
61 | return next();
62 | }
63 | };
64 |
65 | module.exports = pgController;
66 |
--------------------------------------------------------------------------------
/client/components/URI.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from "react";
2 | import { Link, useNavigate } from "react-router-dom";
3 |
4 | const URI = (props) => {
5 | const { id, path, method, status, checked, addToTracking, removeFromTracking } = props;
6 |
7 | const handleClick = () => {
8 | if (checked) removeFromTracking(props.method, props.path);
9 | else addToTracking(props.method, props.path);
10 | return;
11 | };
12 |
13 | return (
14 | <>
15 |
16 | |
17 |
23 | |
24 |
25 |
35 | {path}
36 |
37 | |
38 | {method} |
39 |
40 | 200 && status < 400
45 | ? { backgroundColor: "yellow" }
46 | : { backgroundColor: "red" }
47 | }
48 | >
49 | {status}
50 |
51 | |
52 |
53 |
57 | |
58 |
59 | >
60 | );
61 | };
62 |
63 | export default URI;
64 |
--------------------------------------------------------------------------------
/server/models/influx-client.js:
--------------------------------------------------------------------------------
1 | const { InfluxDB, Point } = require("@influxdata/influxdb-client");
2 | const dotenv = require("dotenv");
3 | const path = require("path");
4 | dotenv.config({ path: path.resolve(__dirname, "../../.env") });
5 |
6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN;
7 | const org = process.env.DB_INFLUXDB_INIT_ORG;
8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET;
9 |
10 | const insertToDB = () => {
11 | // create a new instance of influxDB, providing URL and API token
12 | const client = new InfluxDB({ url: "http://localhost:8086", token: token });
13 | // create a write client, providing influxDB organization and bucket name
14 | const writeApi = client.getWriteApi(org, bucket, "ns");
15 | // create default tags to all points
16 | // writeApi.useDefaultTags({endpoint: '/signup'})
17 |
18 | // use the point constructor passing in "measurement" (table)
19 | const point = new Point("metrics")
20 | .tag("path", "/good")
21 | .tag("method", "GET")
22 | .floatField("res_time", 60)
23 | .intField("status_code", 200);
24 | // .timestamp()
25 |
26 | writeApi.writePoint(point);
27 |
28 | writeApi.close().then(() => {
29 | console.log("WRITE FINISHED");
30 | });
31 | };
32 |
33 | const insertMultiple = (pointsArr) => {
34 | try {
35 | const client = new InfluxDB({
36 | url: "http://localhost:8086",
37 | token: token,
38 | options: {
39 | headers: { "Content-Encoding": "gzip" }
40 | }
41 | });
42 | const writeApi = client.getWriteApi(org, bucket, "ms", {
43 | gzipThreshold: 0,
44 | headers: { "Content-Encoding": "gzip" }
45 | });
46 | writeApi.writePoints(pointsArr);
47 | writeApi.close();
48 | return true;
49 | } catch (e) {
50 | return false;
51 | }
52 | };
53 |
54 | const insertRegistration = (point) => {
55 | const client = new InfluxDB({ url: "http://localhost:8086", token: token });
56 | const writeApi = client.getWriteApi(org, bucket, "ns");
57 |
58 | writeApi.writePoint(point);
59 | writeApi.close().then(() => {
60 | console.log("WRITE FINISHED");
61 | });
62 | };
63 |
64 | module.exports = { insertToDB, insertMultiple, insertRegistration };
65 |
--------------------------------------------------------------------------------
/client/containers/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@mui/material";
2 | import React from "react";
3 | import { useLocation } from "react-router-dom";
4 | import ChartsContainer from "./ChartsContainer.jsx";
5 |
6 | const Dashboard = (props) => {
7 |
8 | const { workspaceId, endpointId, name, domain, port, method, path, isMonitoring, setIsMonitoring } = useLocation().state;
9 |
10 | return (
11 | <>
12 |
19 | {
28 | switch (method) {
29 | case "GET": return "green"
30 | case "POST": return "yellow"
31 | case "PUT": return "blue"
32 | case "PATCH": return "grey"
33 | case "DELETE": return "red"
34 | default: return "grey"
35 | }
36 | })(method)),
37 | borderRadius: 1.5,
38 | px: 1,
39 | py: 0.25,
40 | }}
41 | >
42 |
43 | {method}
44 |
45 |
46 |
52 | {`http://${domain}${typeof port === "number" ? ':' + port : ''}${path}`}
53 |
54 |
55 |
65 | {/* */}
70 | >
71 | );
72 | };
73 |
74 | export default Dashboard;
75 |
--------------------------------------------------------------------------------
/client/components/Histogram.jsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "@mui/material";
2 | import React from "react";
3 | import { tokens } from "../theme";
4 |
5 | import {
6 | BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title,
7 | Tooltip
8 | } from "chart.js";
9 | import { Bar } from "react-chartjs-2";
10 |
11 | const Histogram = (props) => {
12 |
13 | const theme = useTheme();
14 | const colors = tokens(theme.palette.mode);
15 |
16 | const removeEmptyBins = (histData) => {
17 | const newData = [];
18 | for (let i = 0; i < histData.length; i++) {
19 | const someInLaterBins = histData.slice((i > 0 ? i - 1 : 0)).some((dataPoint) => dataPoint.y > 0)
20 | if (someInLaterBins) {
21 | newData.push(histData[i]);
22 | } else {
23 | break;
24 | }
25 | }
26 | return newData;
27 | };
28 |
29 | const chartData = removeEmptyBins(props.chartData) || [];
30 |
31 | ChartJS.register(
32 | CategoryScale,
33 | LinearScale,
34 | BarElement,
35 | Title,
36 | Tooltip,
37 | Legend
38 | );
39 |
40 | const data = {
41 | labels: chartData.map((point) => point.x),
42 | datasets: [
43 | {
44 | label: "Frequency",
45 | data: chartData.map((point) => point.y),
46 | backgroundColor: chartData
47 | .map((point) => point.x)
48 | .map((e, i) => {
49 | return `rgba(${64 + 32 * i}, ${255 - 16 * i}, 64, 0.8)`;
50 | }),
51 | barPercentage: 1.0,
52 | categoryPercentage: 1.0,
53 | borderWidth: 1.0
54 | }
55 | ]
56 | };
57 |
58 | const options = {
59 | responsive: true,
60 | maintainAspectRatio: false,
61 | resizeDelay: 200,
62 | plugins: {
63 | legend: {
64 | display: false,
65 | position: "top"
66 | },
67 | title: {
68 | display: true,
69 | text: "Response Time Distribution"
70 | }
71 | }
72 | };
73 |
74 | return (
75 | //
76 |
84 | //
85 | );
86 | };
87 |
88 | export default Histogram;
89 |
--------------------------------------------------------------------------------
/client/containers/Topbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from "react";
2 | import { Box, IconButton, useTheme } from "@mui/material";
3 | import {
4 | LightModeOutlined,
5 | DarkModeOutlined,
6 | Settings as SettingsIcon,
7 | SettingsOutlined,
8 | NotificationsOutlined,
9 | Help
10 | } from "@mui/icons-material";
11 | import { ColorModeContext, tokens } from "../theme.js";
12 | import { Back, Forward } from "../components/NavButtons.jsx";
13 | import Settings from "../components/Settings.jsx";
14 |
15 | // ! Delete when done
16 | import { useNavigate } from "react-router-dom";
17 |
18 | const TopBar = (props) => {
19 |
20 | const { showSettingsPopup, setShowSettingsPopup } = props;
21 |
22 | const theme = useTheme();
23 | const colors = tokens(theme.palette.mode);
24 | const colorMode = useContext(ColorModeContext);
25 | return (
26 |
79 | );
80 | };
81 |
82 | export default TopBar;
83 |
--------------------------------------------------------------------------------
/dummy/module/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const responseTime = require("response-time");
3 | const listAllEndpoints = require("express-list-endpoints");
4 |
5 | /**
6 | * Start a new Express server that will store and serve a
7 | * list of logs and endpoints of the target server
8 | */
9 | const app = express();
10 |
11 | /**
12 | * Registed endpoints are stored in memory and can be exported
13 | * through the /endpoints endpoint
14 | */
15 | let endpoints = [];
16 |
17 | /**
18 | * Logs are stored in memory until they are cleared by a request
19 | * made to the DELETE /metrics endpoint
20 | */
21 | let logs = [];
22 |
23 | module.exports = {
24 | gatherMetrics: responseTime((req, res, time) => {
25 | if (req.url) {
26 | logs.push({
27 | date_created: new Date(),
28 | path: req.route?.path,
29 | url: req.url,
30 | method: req.method,
31 | status_code: res.statusCode,
32 | response_time: Number(time.toFixed(3)),
33 | });
34 | }
35 | }),
36 |
37 | registerEndpoint: (req, res, next) => {
38 | return next();
39 | },
40 |
41 | exportEndpoints: (app) => {
42 | const registeredEndpoints = listAllEndpoints(app).filter((endpoint) =>
43 | endpoint.middlewares.includes("registerEndpoint")
44 | );
45 | const formattedEndpoints = [];
46 | for (const unformattedEndpoint of registeredEndpoints)
47 | for (const method of unformattedEndpoint.methods)
48 | formattedEndpoints.push({
49 | path: unformattedEndpoint.path,
50 | method: method,
51 | });
52 | return (endpoints = formattedEndpoints);
53 | },
54 |
55 | exportAllEndpoints: (app) => {
56 | const registeredEndpoints = listAllEndpoints(app);
57 | const formattedEndpoints = [];
58 | for (const unformattedEndpoint of registeredEndpoints)
59 | for (const method of unformattedEndpoint.methods)
60 | formattedEndpoints.push({
61 | path: unformattedEndpoint.path,
62 | method: method,
63 | });
64 | return (endpoints = formattedEndpoints);
65 | },
66 |
67 | startMetricsServer: async function (PORT = 9991) {
68 | app.get("/metrics", (req, res) => {
69 | return res.status(200).json(logs);
70 | });
71 | app.delete("/metrics", (req, res) => {
72 | res.status(200).json(logs);
73 | logs = [];
74 | return;
75 | });
76 | app.get("/endpoints", (req, res) => {
77 | return res.json(endpoints);
78 | });
79 | app.listen(PORT, () => {
80 | console.log(`Metrics server started on port ${PORT}`);
81 | });
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "datadoc",
3 | "version": "1.0.0",
4 | "description": "",
5 | "productName": "DataDoc",
6 | "main": "electron/electron-main.js",
7 | "scripts": {
8 | "start": "NODE_ENV=production nodemon server/server.js & NODE_ENV=production electron .",
9 | "build": "NODE_ENV=production webpack",
10 | "dev": "clear & NODE_ENV=development webpack serve & NODE_ENV=development nodemon server/server.js & electron .",
11 | "dev:mock": "npm run dev & nodemon dummy/server.js",
12 | "test": "jest --verbose",
13 | "electron": "electron ."
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/oslabs-beta/DataDoc.git"
18 | },
19 | "keywords": [],
20 | "author": "Jo Huang https://github.com/jochuang, Jonathan Huang https://github.com/JH51, Jamie Schiff https://github.com/jamieschiff, Mariam Zakariadze https://github.com/mariamzakariadze",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/oslabs-beta/DataDoc/issues"
24 | },
25 | "homepage": "https://github.com/oslabs-beta/DataDoc#readme",
26 | "devDependencies": {
27 | "@babel/core": "^7.20.5",
28 | "@babel/preset-env": "^7.20.2",
29 | "@babel/preset-react": "^7.18.6",
30 | "babel-loader": "^9.1.0",
31 | "css-loader": "^6.7.2",
32 | "electron": "^22.0.0",
33 | "html-webpack-plugin": "^5.5.0",
34 | "jest": "^29.3.1",
35 | "react-router-dom": "^6.4.5",
36 | "sass-loader": "^13.2.0",
37 | "style-loader": "^3.3.1",
38 | "webpack": "^5.75.0",
39 | "webpack-cli": "^5.0.1",
40 | "webpack-dev-server": "^4.11.1",
41 | "webpack-hot-middleware": "^2.25.3"
42 | },
43 | "dependencies": {
44 | "@emotion/react": "^11.10.5",
45 | "@emotion/styled": "^11.10.5",
46 | "@influxdata/influxdb-client": "^1.33.0",
47 | "@mui/icons-material": "^5.11.0",
48 | "@mui/material": "^5.11.1",
49 | "@sendgrid/mail": "^7.7.0",
50 | "chart.js": "^4.0.1",
51 | "chartjs-adapter-moment": "^1.0.1",
52 | "compression": "^1.7.4",
53 | "cors": "^2.8.5",
54 | "dotenv": "^16.0.3",
55 | "dotenv-webpack": "^8.0.1",
56 | "express": "^4.18.2",
57 | "express-endpoints-monitor": "^1.0.0",
58 | "node-fetch": "^2.6.7",
59 | "nodemon": "^2.0.20",
60 | "pg": "^8.8.0",
61 | "prom2json-se": "^0.6.0",
62 | "react": "^18.2.0",
63 | "react-chartjs-2": "^5.0.1",
64 | "react-dom": "^18.2.0",
65 | "react-draggable": "^4.4.5",
66 | "react-pro-sidebar": "^0.7.1",
67 | "react-router": "^6.4.5",
68 | "response-time": "^2.3.2",
69 | "sass": "^1.56.2",
70 | "twilio": "^3.84.0",
71 | "typescript": "^4.9.4",
72 | "uuidv4": "^6.2.13"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/client/components/LineChart.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, TimeScale, Title,
3 | Tooltip
4 | } from "chart.js";
5 | import "chartjs-adapter-moment";
6 | import React from "react";
7 | import { Line } from "react-chartjs-2";
8 |
9 | const LineChart = (props) => {
10 | const { chartData, chartTitle, chartLabel } = props;
11 |
12 | ChartJS.register(
13 | TimeScale,
14 | LinearScale,
15 | PointElement,
16 | LineElement,
17 | Title,
18 | Tooltip,
19 | Legend,
20 | Filler
21 | );
22 |
23 | const data = {
24 | datasets: [
25 | {
26 | label: chartLabel,
27 | data: chartData,
28 | fill: true,
29 | backgroundColor: ["rgba(75, 192, 192, 0.50)"],
30 | borderColor: ["rgb(75, 192, 192)"],
31 | tension: 0.3,
32 | },
33 | ],
34 | };
35 |
36 | const options = {
37 | responsive: true,
38 | maintainAspectRatio: false,
39 | resizeDelay: 200,
40 | plugins: {
41 | title: {
42 | display: true,
43 | text: chartTitle,
44 | },
45 | legend: {
46 | display: false,
47 | position: "bottom"
48 | },
49 | },
50 | maintainAspectRatio: false,
51 | scales: {
52 | x: {
53 | type: "time",
54 | grid: {
55 | display: false,
56 | }
57 | },
58 | y: {
59 | beginAtZero: true,
60 | ticks: {
61 | stepSize: (() => {
62 | if (! chartData) return 1;
63 | const maxYValue = Math.max(...(chartData.map((point) => point.y)));
64 | // console.table(chartData.map((point) => point.y));
65 | for (const stepSize of [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]) {
66 | if (maxYValue / stepSize < 6) return stepSize;
67 | }
68 | })()
69 | }
70 | }
71 | },
72 | animation: {
73 | duration: 1000,
74 | // easing: "linear",
75 | },
76 | animations: {
77 | x: {
78 | duration: 100,
79 | easing: "linear",
80 | },
81 | y: {
82 | duration: 0,
83 | },
84 | }
85 | };
86 |
87 | return (
88 | // <>
89 | // {/* */}
90 | // {/*
*/}
91 | //
95 |
104 | //
105 | // {/*
*/}
106 | // {/*
*/}
107 | // >
108 | );
109 | };
110 |
111 | export default LineChart;
112 |
--------------------------------------------------------------------------------
/client/components/DonutChart.jsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "@mui/material";
2 | import { ArcElement, Chart as ChartJS, Legend, Title, Tooltip } from "chart.js";
3 | import React from "react";
4 | import { Doughnut } from "react-chartjs-2";
5 | import { tokens } from "../theme.js";
6 |
7 | const DonutChart = (props) => {
8 | const { id, chartData } = props;
9 | const theme = useTheme();
10 | const colors = tokens(theme.palette.mode);
11 |
12 | ChartJS.register(ArcElement, Tooltip, Title, Legend);
13 |
14 | const backgroundOpacity = 0.7;
15 | const borderOpacity = 0.25;
16 | const gradientFactor = 4;
17 | const colorMapper = (value, gradientFactor, opacity) => {
18 | if (value === "N/A") {
19 | return `rgba(192, 192, 192, ${opacity})`
20 | }
21 | else if (value < 200)
22 | return `rgba(64, ${
23 | 192 - (Number(value) % 100) * gradientFactor
24 | }, 192, ${opacity})`;
25 | else if (value < 300)
26 | return `rgba(64, ${
27 | 255 - (Number(value) % 200) * gradientFactor
28 | }, 64, ${opacity})`;
29 | else if (value < 400)
30 | return `rgba(${255 - (Number(value) % 300) * gradientFactor},${
31 | 255 - (Number(value) % 300) * gradientFactor
32 | }, 128, ${opacity})`;
33 | else if (value < 500)
34 | return `rgba(${
35 | 255 - (Number(value) % 400) * gradientFactor
36 | }, 64, 64, ${opacity})`;
37 | else if (value < 600)
38 | return `rgba(${
39 | 128 - (Number(value) % 500) * gradientFactor
40 | }, 64, 255, ${opacity})`;
41 | };
42 |
43 | const convertCountToPercentage = (counts) => {
44 | const total = counts.reduce((curr, acc) => curr + acc, 0);
45 | return counts.map(count => count / total * 100);
46 | }
47 |
48 | const data = {
49 | labels: chartData.map((point) => String(point.x)),
50 | datasets: [
51 | {
52 | label: "Percentage",
53 | data: convertCountToPercentage(chartData.map((point) => point.y)),
54 | backgroundColor: chartData
55 | .map((point) => point.x)
56 | .map((status_code) =>
57 | colorMapper(status_code, gradientFactor, backgroundOpacity)
58 | ),
59 | borderColor: chartData
60 | .map((point) => point.x)
61 | .map((status_code) =>
62 | colorMapper(status_code, gradientFactor, borderOpacity)
63 | ),
64 | borderWidth: 1,
65 | },
66 | ],
67 | };
68 |
69 | const options = {
70 | responsive: true,
71 | maintainAspectRatio: false,
72 | resizeDelay: 200,
73 | plugins: {
74 | title: {
75 | display: true,
76 | text: 'Status Code Distribution',
77 | },
78 | legend: {
79 | display: true,
80 | position: "left",
81 | align: "center",
82 | labels: {
83 | padding: 0,
84 | }
85 | },
86 | },
87 | };
88 |
89 | return (
90 | //
91 |
99 | //
100 | );
101 | };
102 |
103 | export default DonutChart;
104 |
--------------------------------------------------------------------------------
/client/components/Settings.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import "../styles/Settings.scss";
3 |
4 | const { SERVER_URL } = process.env;
5 |
6 | const Settings = (props) => {
7 | const {
8 | showsettingspopup: showSettingsPopup,
9 | setshowsettingspopup: setShowSettingsPopup
10 | } = props;
11 | const [settingValues, setSettingValues] = useState({
12 | subscribers: [],
13 | status300: false,
14 | status400: false,
15 | status500: false
16 | });
17 |
18 | function handleChange(e, updateValue) {
19 | let settingsUpdate;
20 | let updatedState;
21 | if (
22 | updateValue === "status300" ||
23 | updateValue === "status400" ||
24 | updateValue === "status500"
25 | ) {
26 | settingsUpdate = { [updateValue]: e.target.checked };
27 | updatedState = {
28 | ...settingValues,
29 | ...settingsUpdate
30 | };
31 | } else {
32 | settingsUpdate = { [updateValue]: e.target.value };
33 | updatedState = {
34 | ...settingValues,
35 | ...settingsUpdate
36 | };
37 | }
38 | setSettingValues(updatedState);
39 | }
40 | //send a post request to update the settings
41 | function handleSubmit(e) {
42 | e.preventDefault();
43 | fetch(`${SERVER_URL}/registration`, {
44 | method: "POST",
45 | headers: { "Content-Type": "application/json" },
46 | body: JSON.stringify(settingValues)
47 | }).then(() => {
48 | setSettingValues({
49 | subscribers: [],
50 | status300: false,
51 | status400: false,
52 | status500: false
53 | });
54 | });
55 | }
56 | //settings form
57 | const SettingsForm = () => (
58 |
117 | );
118 | return (
119 | {showSettingsPopup && }
120 | );
121 | };
122 |
123 | export default Settings;
124 |
--------------------------------------------------------------------------------
/client/containers/DrawerContents.jsx:
--------------------------------------------------------------------------------
1 | import { Home, Settings as SettingsIcon } from "@mui/icons-material";
2 | import {
3 | Avatar,
4 | Divider, List,
5 | ListItem,
6 | ListItemButton, ListItemText
7 | } from "@mui/material";
8 | import React, { useEffect, useState } from "react";
9 | import { useNavigate } from "react-router-dom";
10 | import Settings from "../components/Settings.jsx";
11 |
12 | const DrawerContents = (props) => {
13 | const {
14 | open,
15 | showsettingspopup: showSettingsPopup,
16 | setshowsettingspopup: setShowSettingsPopup
17 | } = props;
18 | const [workspaceList, setWorkspaceList] = useState([]);
19 | const navigate = useNavigate();
20 |
21 | useEffect(() => {
22 | getWorkSpaceList();
23 | return;
24 | }, [open]);
25 |
26 | const getWorkSpaceList = async () => {
27 | const newWorkspaceList = await (
28 | await fetch(`http://localhost:${process.env.PORT}/workspaces`)
29 | ).json() || [];
30 | setWorkspaceList(newWorkspaceList);
31 | return;
32 | };
33 |
34 | return (
35 |
36 |
37 | navigate("/")}>
38 |
46 |
47 |
48 |
53 |
54 |
55 |
56 | {workspaceList.map((workspace) => {
57 | return (
58 |
63 | {
70 | navigate(`/workspace/${workspace._id}`, {
71 | state: {
72 | workspaceId: workspace._id,
73 | name: workspace.name,
74 | domain: workspace.domain,
75 | port: workspace.port,
76 | metricsPort: workspace.metrics_port
77 | }
78 | });
79 | }}
80 | >
81 |
90 | {workspace.name
91 | .split(" ")
92 | .slice(0, 2)
93 | .map((word) => word[0])
94 | .join("")
95 | .toUpperCase()}
96 |
97 |
103 |
104 |
105 | );
106 | })}
107 |
108 | {
110 | if (showSettingsPopup) setShowSettingsPopup(false);
111 | else setShowSettingsPopup(true);
112 | }}
113 | >
114 |
122 |
123 |
124 |
125 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | export default DrawerContents;
137 |
--------------------------------------------------------------------------------
/client/containers/ChartsContainer.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Card, ToggleButton, ToggleButtonGroup, useTheme } from "@mui/material";
2 | import Grid from "@mui/material/Unstable_Grid2";
3 | import React, { useEffect, useState } from "react";
4 | import DonutChart from "../components/DonutChart.jsx";
5 | import Histogram from "../components/Histogram.jsx";
6 | import LineChart from "../components/LineChart.jsx";
7 | import "../styles/Charts.scss";
8 | import { tokens } from "../theme.js";
9 |
10 | const { SERVER_URL } = process.env;
11 |
12 | const ChartsContainer = (props) => {
13 | const {
14 | workspaceId,
15 | name,
16 | domain,
17 | port,
18 | metricsPort,
19 | endpointId,
20 | method,
21 | path,
22 | isMonitoring
23 | } = props;
24 | const [chartsData, setChartsData] = useState({});
25 | const [range, setRange] = useState("1m");
26 |
27 | const handleRangeChange = (event, newRange) => newRange ? setRange(newRange) : setRange(range);
28 |
29 | const theme = useTheme();
30 | const colors = tokens(theme.palette.mode);
31 |
32 | useEffect(() => {
33 | if (isMonitoring) {
34 | setInterval(() => {
35 | const encodedPath = path.replaceAll("/", "%2F");
36 | fetch(
37 | `${SERVER_URL}/chartdata/?workspaceId=${workspaceId}&method=${method}&path=${encodedPath}`,
38 | {
39 | method: "GET",
40 | headers: { "Content-Encoding": "gzip" }
41 | }
42 | )
43 | .then((response) => response.json())
44 | .then((dataObj) => {
45 | setChartsData(dataObj);
46 | })
47 | .catch((err) => {
48 | console.log(
49 | `there was an error in the charts container fetch request, error: ${err}`
50 | );
51 | });
52 | }, 2000);
53 | }
54 | }, []);
55 |
56 | useEffect(() => {
57 | fetch(`${SERVER_URL}/chartData/`, {
58 | method: "POST",
59 | headers: { "Content-Type": "application/json" },
60 | body: JSON.stringify({ range })
61 | })
62 | }, [range])
63 |
64 | const cardStyle = {
65 | display: "flex",
66 | flexDirection: "column",
67 | justifyContent: "center",
68 | height: "270px",
69 | width: "100%",
70 | borderRadius: 3,
71 | p: 3,
72 | backgroundColor: `${colors.secondary[100]}`
73 | };
74 |
75 | const toggleButtonGroupStyle = {
76 | alignSelf: 'end',
77 | mb: 2,
78 | };
79 |
80 | const toggleButtonStyle = {
81 | borderRadius: 2,
82 | px: 1.5,
83 | py: 0.375,
84 | }
85 |
86 | return (
87 | <>
88 |
92 |
100 | {/* 30s */}
101 | 1m
102 | 5m
103 | 30m
104 |
105 |
106 | {/* {orderedGridItems} */}
107 |
108 |
109 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
130 |
131 |
132 |
135 |
136 |
137 |
138 |
139 | >
140 | );
141 | };
142 |
143 | export default ChartsContainer;
144 |
--------------------------------------------------------------------------------
/client/containers/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { ProSidebar, Menu, MenuItem } from "react-pro-sidebar";
3 | // import "react-pro-sidebar/dist/css/styles.css";
4 | import { Box, IconButton, Typography, useTheme } from "@mui/material";
5 | // import { Link } from "react-router-dom";
6 | import { tokens } from "../theme.js";
7 | import MenuOutlinedIcon from "@mui/icons-material/MenuOutlined";
8 | // import HomeOutlinedIcon from "@mui/icons-material/HomeOutlined";
9 | // import PeopleOutlinedIcon from "@mui/icons-material/PeopleOutlined";
10 | // import ContactsOutlinedIcon from "@mui/icons-material/ContactsOutlined";
11 | // import ReceiptOutlinedIcon from "@mui/icons-material/ReceiptOutlined";
12 | // import PersonOutlinedIcon from "@mui/icons-material/PersonOutlined";
13 | // import CalendarTodayOutlinedIcon from "@mui/icons-material/CalendarTodayOutlined";
14 | // import HelpOutlineOutlinedIcon from "@mui/icons-material/HelpOutlineOutlined";
15 | // import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined";
16 | // import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined";
17 | // import TimelineOutlinedIcon from "@mui/icons-material/TimelineOutlined";
18 | // import MapOutlinedIcon from "@mui/icons-material/MapOutlined";
19 |
20 | // const Item = ({ title, to, icon, selected, setSelected }) => {
21 | // const theme = useTheme();
22 | // const colors = tokens(theme.palette.mode);
23 | // return (
24 | //
35 | // );
36 | // };
37 |
38 | const Sidebar = () => {
39 | const theme = useTheme();
40 | const colors = tokens(theme.palette.mode);
41 | const [isCollapsed, setIsCollapsed] = useState(false);
42 | const [selected, setSelected] = useState("Dashboard");
43 | return (
44 |
63 |
64 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default Sidebar;
124 |
--------------------------------------------------------------------------------
/client/containers/SimulationView.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Box, Slider, Typography, Button, ButtonGroup } from "@mui/material";
3 | import { useLocation } from "react-router-dom";
4 |
5 | const SimulationView = (props) => {
6 | const [settings, setSettings] = useState({
7 | RPS: 100,
8 | timeInterval: 1,
9 | setTime: 3
10 | });
11 |
12 | const location = useLocation();
13 | const { domain, port, path, method, metricsPort, workspaceId } =
14 | location.state;
15 |
16 | function valuetext(value) {
17 | return `${value}`;
18 | }
19 |
20 | function handleChange(e, updatedVal) {
21 | const updatedInputVal = { [updatedVal]: e.target.value };
22 | const updatedState = {
23 | ...settings,
24 | ...updatedInputVal
25 | };
26 | setSettings(updatedState);
27 | }
28 |
29 | function handleTesting(e) {
30 | e.preventDefault();
31 | fetch(`http://localhost:${process.env.PORT}/simulation`, {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json"
35 | },
36 | body: JSON.stringify({
37 | ...settings,
38 | domain,
39 | port,
40 | path,
41 | method,
42 | mode: "simulation",
43 | metricsPort,
44 | workspaceId,
45 | stop: false
46 | })
47 | })
48 | .then((res) => res.json())
49 | .then((data) => {
50 | // console.log('THIS IS FROM THE RESPONSE', data)
51 | })
52 | .catch((err) => {
53 | console.log(
54 | `there was an error sending the simulation METRICS, error: ${err}`
55 | );
56 | });
57 | }
58 |
59 | function handleStop(e) {
60 | e.preventDefault();
61 | fetch(`http://localhost:${process.env.PORT}/simulation`, {
62 | method: "POST",
63 | headers: {
64 | "Content-Type": "application/json"
65 | },
66 | body: JSON.stringify({
67 | ...settings,
68 | path: path,
69 | stop: true,
70 | metricsPort,
71 | mode: "simulation",
72 | workspaceId,
73 | })
74 | })
75 | .then((res) => res.json())
76 | .then((data) => {
77 | console.log("THIS IS FROM THE RESPONSE", data);
78 | })
79 | .catch((err) => {
80 | console.log(
81 | `there was an error sending the simulation METRICS, error: ${err}`
82 | );
83 | });
84 | }
85 |
86 | return (
87 |
88 |
89 |
90 | Simulate User Activity
91 |
92 |
93 | Requests Per Second:
94 |
95 | handleChange(e, "RPS")}
108 | />
109 |
110 | Time Interval:
111 |
112 | handleChange(e, "timeInterval")}
125 | />
126 |
127 | Time Elapsed, Minutes:
128 |
129 | handleChange(e, "setTime")}
142 | />
143 |
144 |
151 |
152 |
161 |
171 |
172 |
173 |
174 |
175 | );
176 | };
177 |
178 | export default SimulationView;
179 |
--------------------------------------------------------------------------------
/client/containers/WorkspaceView.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useState, useEffect } from "react";
3 | import { useLocation, useNavigate } from "react-router-dom";
4 | import { IconButton } from "@mui/material";
5 | import { ChevronRight, Launch } from "@mui/icons-material";
6 | import WorkspaceInfo from "../components/WorkspaceInfo.jsx"
7 | import URITable from "../components/URITable.jsx";
8 |
9 | const WorkspaceView = () => {
10 | const location = useLocation();
11 | const { workspaceId, name, domain, port } = location.state;
12 | const [URIList, setURIList] = useState([]);
13 | const [isMonitoring, setIsMonitoring] = useState();
14 | const [metricsPort, setMetricsPort] = useState(location.state.metricsPort);
15 |
16 | const navigate = useNavigate();
17 |
18 | useEffect(() => {
19 | fetch(`${process.env.SERVER_URL}/monitoring/${workspaceId}`)
20 | .then((serverResponse) => {
21 | return serverResponse.json();
22 | })
23 | .then((responseJson) => {
24 | setIsMonitoring(responseJson);
25 | })
26 | })
27 |
28 | useEffect(() => {
29 | getURIListFromDatabase(workspaceId);
30 | }, [workspaceId]);
31 |
32 | const addURIListToDatabase = async (workspaceId, URIList = URIList) => {
33 | await fetch(`${process.env.SERVER_URL}/routes/${workspaceId}`, {
34 | method: 'POST',
35 | headers: {
36 | "Content-Type": "application/json"
37 | },
38 | body: JSON.stringify(URIList),
39 | })
40 | }
41 |
42 | const getURIListFromServer = (metricsPortArg) => {
43 | fetch(
44 | `http://localhost:${process.env.PORT}/routes/server?metricsPort=${metricsPortArg}`
45 | )
46 | .then((response) => response.json())
47 | .then((data) => {
48 | setURIList(data);
49 | })
50 | .catch((err) => {
51 | setErrorMessage("Invalid server fetch request for the URI List");
52 | // * reset the error message
53 | setTimeout(() => setErrorMessage(""), 5000);
54 | });
55 | };
56 |
57 | const getURIListFromDatabase = (workspaceId) => {
58 | fetch(`http://localhost:${process.env.PORT}/routes/${workspaceId}`)
59 | .then((response) => response.json())
60 | .then((data) => {
61 | setURIList(data);
62 | })
63 | .catch((err) => {
64 | setErrorMessage("Invalid db fetch request for the URI List");
65 | // * reset the error message
66 | setTimeout(() => setErrorMessage(""), 5000);
67 | });
68 | };
69 |
70 | const deleteURIListFromDatabase = (workspaceId) => {
71 | fetch(`http://localhost:${process.env.PORT}/endpoints/${workspaceId}`, {
72 | method: 'DELETE',
73 | })
74 | .catch((err) => {
75 | setErrorMessage("Invalid db DELETE request for the URI List");
76 | // * reset the error message
77 | setTimeout(() => setErrorMessage(""), 5000);
78 | });
79 | };
80 |
81 | const updateTrackingInDatabaseById = async (updatedEndpoint) => {
82 | fetch(`http://localhost:9990/endpoints/${updatedEndpoint._id}`, {
83 | method: `PUT`,
84 | headers: { "Content-Type": "application/json" },
85 | body: JSON.stringify(updatedEndpoint)
86 | }).then((serverResponse) => {
87 | if (serverResponse.ok) {
88 | const updatedURIList = URIList.map((URI) => {
89 | return URI._id === updatedEndpoint._id ? updatedEndpoint : URI;
90 | });
91 | setURIList(updatedURIList);
92 | }
93 | });
94 | return;
95 | };
96 |
97 | // const updateTrackingInDatabaseByRoute = async (updatedEndpoint) => {
98 | // fetch(`http://localhost:9990/endpoints2`, {
99 | // method: `PUT`,
100 | // headers: { "Content-Type": "application/json" },
101 | // body: JSON.stringify(updatedEndpoint)
102 | // }).then((serverResponse) => {
103 | // if (serverResponse.ok) {
104 | // getURIListFromDatabase(workspaceId)
105 | // }
106 | // });
107 | // return;
108 | // };
109 |
110 | const refreshURIList = async (workspaceId = workspaceId, metricsPort = metricsPort) => {
111 | const response = await fetch(`${process.env.SERVER_URL}/routes/server?workspaceId=${workspaceId}&metricsPort=${metricsPort}`, {
112 | method: "PUT",
113 | headers: {
114 | "Content-type": "application/json"
115 | },
116 | body: JSON.stringify({
117 | metricsPort,
118 | workspaceId,
119 | })
120 | })
121 | const data = await response.json();
122 | setURIList(data);
123 | }
124 |
125 | return (
126 | <>
127 |
138 | {
147 | return {
148 | _id: URI._id, // hidden column
149 | _tracking: URI.tracking, // hidden column
150 | path: URI.path,
151 | method: URI.method,
152 | status_code: URI.statusCode || "N/A",
153 | simulation:
154 | {
156 | navigate(`/simulation/${crypto.randomUUID()}`, {
157 | state: {
158 | workspaceId,
159 | domain,
160 | port,
161 | metricsPort,
162 | path: URI.path,
163 | method: URI.method,
164 | }
165 | })
166 | }}
167 | >
168 |
171 | ,
172 | open:
173 | {
175 | const url = `http://${domain}${typeof port === "number" ? ':' + port : ""}${URI.path}`
176 | window.open(url);
177 | }}
178 | >
179 |
182 |
183 | };
184 | })}
185 | updateTrackingInDatabaseById={updateTrackingInDatabaseById}
186 | getURIListFromServer={getURIListFromServer}
187 | refreshURIList={refreshURIList}
188 | />
189 | >
190 | );
191 | };
192 |
193 | export default WorkspaceView;
--------------------------------------------------------------------------------
/client/theme.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useMemo } from "react";
2 | import { createTheme } from "@mui/material/styles";
3 |
4 | export const tokens = (mode) => ({
5 | ...(mode === "dark"
6 | ? {
7 | grey: {
8 | 100: "#e0e0e0",
9 | 200: "#c2c2c2",
10 | 300: "#a3a3a3",
11 | 400: "#858585",
12 | 500: "#666666",
13 | 600: "#525252",
14 | 700: "#3d3d3d",
15 | 800: "#292929",
16 | 900: "#141414",
17 | },
18 | primary: {
19 | 100: "#d0d1d5",
20 | 200: "#a1a4ab",
21 | 300: "#727681",
22 | 400: "#1F2A40",
23 | 500: "#141b2d",
24 | 600: "#101624",
25 | 700: "#0c101b",
26 | 800: "#080b12",
27 | 900: "#040509",
28 | },
29 | secondary: {
30 | 100: "#13294B",
31 | },
32 | greenAccent: {
33 | 100: "#dbf5ee",
34 | 200: "#b7ebde",
35 | 300: "#94e2cd",
36 | 400: "#70d8bd",
37 | 500: "#4cceac",
38 | 600: "#3da58a",
39 | 700: "#2e7c67",
40 | 800: "#1e5245",
41 | 900: "#0f2922",
42 | },
43 | redAccent: {
44 | 100: "#f8dcdb",
45 | 200: "#f1b9b7",
46 | 300: "#e99592",
47 | 400: "#e2726e",
48 | 500: "#db4f4a",
49 | 600: "#af3f3b",
50 | 700: "#832f2c",
51 | 800: "#58201e",
52 | 900: "#2c100f",
53 | },
54 | blueAccent: {
55 | 100: "#e1e2fe",
56 | 200: "#c3c6fd",
57 | 300: "#a4a9fc",
58 | 400: "#868dfb",
59 | 500: "#6870fa",
60 | 600: "#535ac8",
61 | 700: "#3e4396",
62 | 800: "#2a2d64",
63 | 900: "#151632",
64 | },
65 | }
66 | : {
67 | grey: {
68 | 100: "#141414",
69 | 200: "#292929",
70 | 300: "#3d3d3d",
71 | 400: "#525252",
72 | 500: "#666666",
73 | 600: "#858585",
74 | 700: "#a3a3a3",
75 | 800: "#c2c2c2",
76 | 900: "#e0e0e0",
77 | },
78 | primary: {
79 | 100: "#040509",
80 | 200: "#080b12",
81 | 300: "#0c101b",
82 | 400: "#f2f0f0",
83 | 500: "#141b2d",
84 | 600: "#434957",
85 | 700: "#727681",
86 | 800: "#a1a4ab",
87 | 900: "#d0d1d5",
88 | },
89 | secondary: {
90 | 100: "#FFFFFF",
91 | },
92 | greenAccent: {
93 | 100: "#0f2922",
94 | 200: "#1e5245",
95 | 300: "#2e7c67",
96 | 400: "#3da58a",
97 | 500: "#4cceac",
98 | 600: "#70d8bd",
99 | 700: "#94e2cd",
100 | 800: "#b7ebde",
101 | 900: "#dbf5ee",
102 | },
103 | redAccent: {
104 | 100: "#2c100f",
105 | 200: "#58201e",
106 | 300: "#832f2c",
107 | 400: "#af3f3b",
108 | 500: "#db4f4a",
109 | 600: "#e2726e",
110 | 700: "#e99592",
111 | 800: "#f1b9b7",
112 | 900: "#f8dcdb",
113 | },
114 | blueAccent: {
115 | 100: "#151632",
116 | 200: "#2a2d64",
117 | 300: "#3e4396",
118 | 400: "#535ac8",
119 | 500: "#6870fa",
120 | 600: "#868dfb",
121 | 700: "#a4a9fc",
122 | 800: "#c3c6fd",
123 | 900: "#e1e2fe",
124 | },
125 | }),
126 | });
127 |
128 | // mui theme settings
129 | export const themeSettings = (mode) => {
130 | const colors = tokens(mode);
131 | return {
132 | breakpoints: {
133 | values: {
134 | xs: 0, //0
135 | sm: 600, //600
136 | md: 900, //900
137 | lg: 1200, //1200
138 | xl: 1500, //1536
139 | },
140 | },
141 | palette: {
142 | mode: mode,
143 | ...(mode === "dark"
144 | ? {
145 | // palette values for dark mode
146 | primary: {
147 | main: colors.primary[500],
148 | },
149 | secondary: {
150 | main: colors.greenAccent[600],
151 | },
152 | neutral: {
153 | dark: colors.grey[700],
154 | main: colors.grey[500],
155 | light: colors.grey[100],
156 | },
157 | background: {
158 | default: colors.primary[500],
159 | },
160 | customRed: {
161 | main: "rgb(255, 64, 64)"
162 | },
163 | disabled: {
164 | main: colors.grey[700],
165 | },
166 | }
167 | : {
168 | // palette values for light mode
169 | primary: {
170 | main: colors.primary[100],
171 | },
172 | secondary: {
173 | main: colors.greenAccent[500],
174 | },
175 | neutral: {
176 | dark: colors.grey[700],
177 | main: colors.grey[700],
178 | light: colors.grey[100],
179 | },
180 | background: {
181 | default: "#fcfcfc",
182 | },
183 | customRed: {
184 | main: "#FF0000"
185 | },
186 | disabled: {
187 | main: colors.grey[800],
188 | },
189 | }),
190 | },
191 | typography: {
192 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
193 | fontSize: 12,
194 | h1: {
195 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
196 | fontSize: 40,
197 | },
198 | h2: {
199 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
200 | fontSize: 32,
201 | },
202 | h3: {
203 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
204 | fontSize: 24,
205 | },
206 | h4: {
207 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
208 | fontSize: 20,
209 | },
210 | h5: {
211 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
212 | fontSize: 16,
213 | },
214 | h6: {
215 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","),
216 | fontSize: 14,
217 | },
218 | },
219 | };
220 | };
221 |
222 | // context for color mode
223 | export const ColorModeContext = createContext({
224 | toggleColorMode: () => {},
225 | });
226 |
227 | export const useMode = () => {
228 | const [mode, setMode] = useState("light");
229 |
230 | const colorMode = useMemo(
231 | () => ({
232 | toggleColorMode: () =>
233 | setMode((prev) => (prev === "light" ? "dark" : "light")),
234 | }),
235 | []
236 | );
237 |
238 | const theme = useMemo(() => createTheme(themeSettings(mode)), [mode]);
239 | return [theme, colorMode];
240 | };
241 |
--------------------------------------------------------------------------------
/client/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import { CssBaseline, ThemeProvider } from "@mui/material";
2 | import React, { useEffect, useState } from "react";
3 | import { ColorModeContext, useMode } from "../theme.js";
4 |
5 | import Home from "../components/Home.jsx";
6 | import WorkspaceCard from "../components/WorkspaceCard.jsx";
7 | import WorkspaceView from "./WorkspaceView.jsx";
8 | import Settings from "../components/Settings.jsx";
9 | import URI from "../components/URI.jsx";
10 | import WorkspaceInfo from "../components/WorkspaceInfo.jsx";
11 | import Dashboard from "./Dashboard.jsx";
12 | import SimulationView from "./SimulationView.jsx";
13 | import DrawerContents from "./DrawerContents.jsx";
14 | // import NavBar from "./NavBar.jsx";
15 | import SideBar from "./NavBar.jsx";
16 | import TopBar from "./TopBar.jsx";
17 | import { HashRouter as Router, Route, Routes } from "react-router-dom";
18 |
19 | import "../styles/globals.scss";
20 |
21 | import { styled, useTheme } from "@mui/material/styles";
22 | import { Menu, ChevronLeft, ChevronRight } from "@mui/icons-material";
23 | import {
24 | Box,
25 | Divider,
26 | Drawer as MuiDrawer,
27 | AppBar as MuiAppBar,
28 | Toolbar,
29 | IconButton,
30 | List,
31 | Typography,
32 | ListItem,
33 | ListItemButton,
34 | ListItemIcon,
35 | ListItemText
36 | } from "@mui/material";
37 |
38 | const drawerWidth = 270;
39 | const openedMixin = (theme) => ({
40 | width: drawerWidth,
41 | transition: theme.transitions.create("width", {
42 | easing: theme.transitions.easing.sharp,
43 | duration: theme.transitions.duration.enteringScreen
44 | }),
45 | overflowX: "hidden"
46 | });
47 | const closedMixin = (theme) => ({
48 | transition: theme.transitions.create("width", {
49 | easing: theme.transitions.easing.sharp,
50 | duration: theme.transitions.duration.leavingScreen
51 | }),
52 | overflowX: "hidden",
53 | width: `calc(${theme.spacing(7)} + 1px)`,
54 | [theme.breakpoints.up("sm")]: {
55 | width: `calc(${theme.spacing(8)} + 1px)`
56 | }
57 | });
58 | const DrawerSection = styled("div")(({ theme }) => ({
59 | display: "flex",
60 | alignItems: "center",
61 | justifyContent: "flex-end",
62 | padding: theme.spacing(0, 1),
63 | // necessary for content to be below app bar
64 | ...theme.mixins.toolbar
65 | }));
66 | const AppBar = styled(MuiAppBar, {
67 | shouldForwardProp: (prop) => prop !== "open"
68 | })(({ theme, open }) => ({
69 | zIndex: theme.zIndex.drawer + 1,
70 | transition: theme.transitions.create(["width", "margin"], {
71 | easing: theme.transitions.easing.sharp,
72 | duration: theme.transitions.duration.leavingScreen
73 | }),
74 | ...(open && {
75 | marginLeft: drawerWidth,
76 | width: `calc(100% - ${drawerWidth}px)`,
77 | transition: theme.transitions.create(["width", "margin"], {
78 | easing: theme.transitions.easing.sharp,
79 | duration: theme.transitions.duration.enteringScreen
80 | })
81 | })
82 | }));
83 | const Drawer = styled(MuiDrawer, {
84 | shouldForwardProp: (prop) => prop !== "open"
85 | })(({ theme, open }) => ({
86 | width: drawerWidth,
87 | flexShrink: 0,
88 | whiteSpace: "nowrap",
89 | boxSizing: "border-box",
90 | ...(open && {
91 | ...openedMixin(theme),
92 | "& .MuiDrawer-paper": openedMixin(theme)
93 | }),
94 | ...(!open && {
95 | ...closedMixin(theme),
96 | "& .MuiDrawer-paper": closedMixin(theme)
97 | })
98 | }));
99 |
100 | const App = () => {
101 | const [theme, colorMode] = useMode();
102 | const [open, setOpen] = useState(false);
103 | const [showSettingsPopup, setShowSettingsPopup] = useState(false);
104 | const handleDrawerOpen = () => setOpen(true);
105 | const handleDrawerClose = () => setOpen(false);
106 |
107 | return (
108 |
109 |
110 |
111 |
112 |
113 |
114 | {/* Top Toolbar */}
115 |
116 |
117 |
127 |
128 |
129 |
133 |
134 |
135 | {/* */}
136 |
137 | {/* Drawer */}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
150 |
151 | {/* */}
152 |
153 |
154 | {/* Main components */}
155 |
156 |
157 |
158 |
159 |
160 |
161 | } />
162 | {/* } /> */}
163 | } />
164 | } />
165 | } />
166 | } />
167 | } />
168 | {/* } /> */}
169 | }
172 | />
173 |
174 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | );
187 | };
188 |
189 | export default App;
190 |
--------------------------------------------------------------------------------
/client/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Box, Card, Container, Typography } from "@mui/material";
3 | import { Add } from "@mui/icons-material";
4 | import Grid from "@mui/material/Unstable_Grid2";
5 | import WorkspaceCard from "./WorkspaceCard.jsx";
6 | import "../styles/AddWorkspace.scss";
7 | import { useTheme } from "@mui/material";
8 | import { tokens } from "../theme.js";
9 |
10 | const { SERVER_URL } = process.env;
11 |
12 | const Home = (props) => {
13 | const theme = useTheme();
14 | const colors = tokens(theme.palette.mode);
15 | const [workspaceList, setWorkspaceList] = useState([]);
16 | const [showNewWorkspacePopUp, setShowNewWorkspacePopUp] = useState(false);
17 | const [workspaceValues, setWorkspaceValues] = useState({
18 | name: "",
19 | domain: "",
20 | port: "",
21 | workspaceId: "",
22 | });
23 |
24 | const handleChange = (e, updateValue) => {
25 | let updatedState;
26 | let workspaceUpdate;
27 | workspaceUpdate = { [updateValue]: e.target.value };
28 | updatedState = {
29 | ...workspaceValues,
30 | ...workspaceUpdate
31 | };
32 | setWorkspaceValues(updatedState);
33 | };
34 |
35 | const handleSubmit = (e) => {
36 | e.preventDefault();
37 | fetch(`${SERVER_URL}/workspaces`, {
38 | method: "POST",
39 | headers: { "Content-Type": "application/json" },
40 | body: JSON.stringify(workspaceValues)
41 | })
42 | .then(() => {
43 | getAllWorkspaces();
44 | })
45 | .then(() => {
46 | setWorkspaceValues({
47 | name: "",
48 | domain: "",
49 | port: 0
50 | });
51 | })
52 | .then(() => {
53 | setShowNewWorkspacePopUp(false);
54 | })
55 | .catch((err) => {
56 | console.log(
57 | `there wan an error submitting a new workspace, err: ${err}`
58 | );
59 | });
60 | };
61 |
62 | //set the values for the new workspace
63 | const newWorkspaceForm = (
64 |
115 | );
116 |
117 | //fetch the workspace list from the backend when the component mounts
118 | useEffect(() => {
119 | getAllWorkspaces();
120 | }, []);
121 |
122 | const getAllWorkspaces = () => {
123 | fetch(`http://localhost:${process.env.PORT}/workspaces`)
124 | .then((response) => {
125 | return response.json();
126 | })
127 | .then((data) => {
128 | setWorkspaceList(data);
129 | })
130 | .catch((err) => {
131 | console.log(`there was an error: ${err}`);
132 | });
133 | };
134 |
135 | const deleteWorkspaceById = (workspaceId) => {
136 | fetch(`${SERVER_URL}/workspaces`, {
137 | method: "DELETE",
138 | headers: {
139 | "Content-Type": "application/json"
140 | },
141 | body: JSON.stringify({ workspace_id: workspaceId })
142 | })
143 | .then((workspace) => {
144 | getAllWorkspaces();
145 | })
146 | .catch((err) => {
147 | console.log(`there was an error deleting a workspace, error: ${err}`);
148 | });
149 | };
150 |
151 | // const deleteSpecificWorkspace = (name) => {
152 | // // console.log("in the process of deleting a workspace");
153 | // const updatedWorkspaceList = workspaceList.filter(
154 | // (item) => item.name !== name
155 | // );
156 | // getWorkSpaceList();
157 | // };
158 |
159 | const cardStyle = {
160 | // boxSizing: 'border-box',
161 | borderRadius: 3,
162 | height: "170px",
163 | minwidth: "60px",
164 | padding: 2,
165 | // boxShadow: "0px 0px 8px 4px rgba(0, 0, 0, 0.02)",
166 | cursor: "pointer",
167 | backgroundColor: `${colors.secondary[100]}`
168 | };
169 |
170 | return (
171 | <>
172 |
173 | Welcome to DataDoc
174 |
175 |
176 | Workspaces:
177 |
178 |
179 | {workspaceList.map((workspace) => {
180 | return (
181 |
190 |
191 |
199 |
200 |
201 | );
202 | })}
203 |
204 | setShowNewWorkspacePopUp(true)}
213 | >
214 |
217 | {showNewWorkspacePopUp && newWorkspaceForm}
218 |
219 |
220 |
221 | >
222 | );
223 | };
224 |
225 | export default Home;
226 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DataDoc
2 |
3 | DataDoc is an endpoint monitoring, detection and traffic simulation tool that provides real-time metrics and customizable alert notifications.
4 |
5 |
6 |
7 | ### Table of Contents
8 | * [Getting Started](#getting-started)
9 | * [Prerequisites](#prerequisites)
10 | * [Installation](#installation)
11 | * [How To Use](#how-to-use)
12 | * [Adding Workspaces](#adding-workspaces)
13 | * [Using the Monitoring Tool](#using-the-monitoring-tool)
14 | * [Using the Simulation Tool](#using-the-simulation-tool)
15 | * [Configuring Alerts](#configuring-alerts)
16 | * [Tech Stack](#tech-stack)
17 | * [Authors](#authors)
18 |
19 | ## Getting Started
20 |
21 | ### Prerequisites
22 |
23 | - Node.js v18^
24 | - Docker
25 |
26 | ### Installation
27 |
28 | This tool requires the npm package `express-endpoints-monitor` to detect and gather metrics for endpoints in your Express application. To understand how to use this plugin, see the full documentation.
29 |
30 | 1. Run the following terminal command in your project directory that you would like to begin monitoring:
31 |
32 | ```
33 | npm install express-endpoints-monitor
34 | ```
35 |
36 | This should have created a `express-endpoints-monitor/` folder in your `node_modules/` directory
37 |
38 | 2. WIP: Clone this repository. Unzip the file in a separate folder and open a terminal in this directory. Run the following commands:
39 |
40 | ```
41 | npm install
42 | npm run build
43 | ```
44 |
45 | This will install the needed dependences and build the desktop application.
46 |
47 | ### Exposing Endpoints to the Monitoring Tool
48 |
49 | 1. Open your Express application file in a text editor. At the top of the file, import the plugin by adding:
50 |
51 | ```
52 | const expMonitor = require("express-endpoints-monitor");
53 | ```
54 |
55 | This module comes with several functions to register endpoints with the monitoring application and begin log requests made to those endpoints.
56 |
57 | 2. In your file, include the following line:
58 |
59 | ```
60 | app.use(expMonitor.gatherMetrics);
61 | ```
62 |
63 | This will record metrics for incoming requests and make them available to the metrics API which will be set up later.
64 |
65 | 3. Under an endpoint that you would like to begin monitoring, include the `expMonitor.registerEndpoint` middleware. For example, this may look like:
66 |
67 | ```
68 | app.get(...,
69 | expMonitor.registerEndpoint,
70 | ...
71 | );
72 | ```
73 |
74 | The order of this function in the middleware chain is not important. This middleware will stage this particular endpoint for exporting, and can be used in multiple endpoints.
75 |
76 | 4. Once all desired endpoints have been registered, they must be exported on the metrics server. In your `app.listen` declaration, add these lines to the passed-in callback function:
77 |
78 | ```
79 | app.listen(..., function callback() {
80 | ...
81 | expMonitor.exportEndpoints();
82 | startMetricsServer()
83 | )
84 | ```
85 |
86 | This will start up a metrics server on `METRICS_SERVER_PORT`. If this argument is not specified, it will resolve to `9991`. The server includes several endpoints, one of which is `GET /endpoints` which responds with the list of registered endpoints in JSON format.
87 |
88 | Alternatively, if you would like to export all endpoints, you may replace the above snippet with the `exportAllEndpoints` function:
89 |
90 | ```
91 | app.listen(..., function callback() {
92 | ...
93 | expMonitor.exportAllEndpoints();
94 | startMetricsServer()
95 | )
96 | ```
97 |
98 | This will expose all endpoints regardless of whether they include the `registerEndpoint` middleware.
99 |
100 | 5. Your application is ready to start monitoring! To verify your setup, use a browser or API testing tool to interact with the metrics API started at `http://localhost:`. The list of available endpoints is:
101 |
102 | - `GET /endpoints`
103 | - `GET /metrics`
104 | - `DELETE /metrics`
105 |
106 | 6. To see the full use of the library, see the npm page.
107 |
108 | ### Initializing Databases
109 |
110 | In your local `DataDoc` folder, run the following command:
111 |
112 | ```
113 | docker compose up
114 | ```
115 |
116 | The `-d` flag may be supplied to detach the instance from the terminal.
117 |
118 | ### Starting the Desktop Application
119 |
120 | 1. In your local `DataDoc` folder, run the following command if you haven't during the installation steps:
121 |
122 | ```
123 | npm build
124 | ```
125 |
126 | This command only needs to be run once.
127 |
128 | 2. WIP: In the same folder, run the following command to start the desktop application:
129 |
130 | ```
131 | npm start
132 | ```
133 | ## How to Use
134 |
135 | ### Adding Workspaces
136 |
137 | ### Using the Monitoring Tool
138 |
139 | ### Using the Simulation Tool
140 |
141 | ### Configuring Alerts
142 |
143 | ## Tech Stack
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | ## Authors
169 |
170 | - Jo Huang LinkedIn | GitHub
171 | - Jonathan Huang LinkedIn | GitHub
172 | - Jamie Schiff LinkedIn | GitHub
173 | - Mariam Zakariadze LinkedIn | GitHub
174 |
--------------------------------------------------------------------------------
/server/controllers/influxController.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const { InfluxDB } = require("@influxdata/influxdb-client");
3 | const path = require("path");
4 | const influxClient = require("../models/influx-client.js");
5 |
6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN;
7 | const org = process.env.DB_INFLUXDB_INIT_ORG;
8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET;
9 |
10 | const queryApi = new InfluxDB({
11 | url: "http://localhost:8086",
12 | token: token
13 | }).getQueryApi({
14 | org,
15 | gzip: true,
16 | headers: {
17 | "Content-Encoding": "gzip"
18 | }
19 | });
20 |
21 | const influxController = {};
22 |
23 | let range = "1m";
24 |
25 | // declare a data object to store chart data
26 | const data = {
27 | respTimeLineData: [],
28 | respTimeHistData: [],
29 | reqFreqLineData: [],
30 | statusPieData: []
31 | };
32 |
33 | influxController.updateRange = (req, res, next) => {
34 | range = req.body.range || range;
35 | console.log(`Updated range to: ${range}`)
36 | return next();
37 | }
38 |
39 | influxController.getRespTimeLineData = (req, res, next) => {
40 | const fluxQuery = `
41 | from(bucket: "dev-bucket")
42 | |> range(start: -${range})
43 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}")
44 | |> filter(fn: (r) => r["_field"] == "res_time")
45 | |> filter(fn: (r) => r["method"] == "${req.query.method}")
46 | |> filter(fn: (r) => r["path"] == "${req.query.path}")
47 | |> yield(name: "mean")
48 | `;
49 |
50 | // declare a metrics object to collect labels and data
51 | const metrics = [];
52 |
53 | queryApi.queryRows(fluxQuery, {
54 | next(row, tableMeta) {
55 | const o = tableMeta.toObject(row);
56 | metrics.push({ x: o._time, y: o._value });
57 | },
58 | error(error) {
59 | console.log("Query Finished ERROR");
60 | return next(error);
61 | },
62 | complete() {
63 | data.respTimeLineData = metrics;
64 | res.locals.data = data;
65 | // console.log("Query Finished SUCCESS");
66 | return next();
67 | }
68 | });
69 | };
70 |
71 | influxController.getRespTimeHistData = (req, res, next) => {
72 | const fluxQuery = `
73 | from(bucket: "dev-bucket")
74 | |> range(start: -${range})
75 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}")
76 | |> filter(fn: (r) => r["_field"] == "res_time")
77 | |> filter(fn: (r) => r["method"] == "${req.query.method}")
78 | |> filter(fn: (r) => r["path"] == "${req.query.path}")
79 | |> histogram(bins: [0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0])
80 | `;
81 |
82 | // declare a metrics object to collect labels and data
83 | const metrics = [];
84 |
85 | // declare a variable 'le' (lower than or equal to) and 'respFreq' to collect labels and data
86 | const le = [];
87 | const respFreq = [];
88 |
89 | queryApi.queryRows(fluxQuery, {
90 | next(row, tableMeta) {
91 | const o = tableMeta.toObject(row);
92 | le.push(o.le);
93 | respFreq.push(o._value);
94 | },
95 | error(error) {
96 | console.log("Query Finished ERROR");
97 | return next(error);
98 | },
99 | complete() {
100 | const newResFreq = respFreq.map((el, i) => {
101 | if (i === 0) return respFreq[i];
102 | return respFreq[i] - respFreq[i - 1];
103 | });
104 | for (let i = 0; i < newResFreq.length; i++) {
105 | metrics.push({ x: le[i], y: newResFreq[i] });
106 | }
107 | data.respTimeHistData = metrics;
108 | res.locals.data = data;
109 | // console.log("Query Finished SUCCESS");
110 | return next();
111 | }
112 | });
113 | };
114 |
115 | influxController.getReqFreqLineData = (req, res, next) => {
116 | const fluxQuery = `
117 | from(bucket: "dev-bucket")
118 | |> range(start: -${range})
119 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}")
120 | |> filter(fn: (r) => r["_field"] == "res_time")
121 | |> filter(fn: (r) => r["method"] == "${req.query.method}")
122 | |> filter(fn: (r) => r["path"] == "${req.query.path}")
123 | |> aggregateWindow(every: ${(() => {switch (range) {
124 | case "1m": return "10s"
125 | case "5m": return "1m"
126 | case "30m": return "5m"
127 | default: return "10s"
128 | }
129 | })()}, fn: count, createEmpty: false)
130 | `;
131 |
132 | // declare a metrics object to collect labels and data
133 | const metrics = [];
134 |
135 | queryApi.queryRows(fluxQuery, {
136 | next(row, tableMeta) {
137 | const o = tableMeta.toObject(row);
138 | metrics.push({ x: o._time, y: o._value });
139 | },
140 | error(error) {
141 | console.log("Query Finished ERROR");
142 | return next(error);
143 | },
144 | complete() {
145 | data.reqFreqLineData = metrics;
146 | res.locals.data = data;
147 | // console.log("Query Finished SUCCESS");
148 | return next();
149 | }
150 | });
151 | };
152 |
153 | influxController.getStatusPieData = (req, res, next) => {
154 | const influxQuery = `
155 | from(bucket: "dev-bucket")
156 | |> range(start: -${range})
157 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}")
158 | |> filter(fn: (r) => r["_field"] == "status_code")
159 | |> filter(fn: (r) => r["method"] == "${req.query.method}")
160 | |> filter(fn: (r) => r["path"] == "${req.query.path}")
161 | |> group(columns: ["_value"])
162 | |> count(column: "_field")
163 | |> group()
164 | `;
165 |
166 | // declare a metrics object to collect labels and data
167 | const metrics = [];
168 |
169 | // declare a stats object to collect labels and data
170 | queryApi.queryRows(influxQuery, {
171 | next(row, tableMeta) {
172 | const o = tableMeta.toObject(row);
173 | metrics.push({ x: o._value, y: o._field });
174 | },
175 | error(error) {
176 | console.log("Query Finished ERROR");
177 | return next(error);
178 | },
179 | complete() {
180 | data.statusPieData = metrics;
181 | res.locals.data = data;
182 | // console.log("Query Finished SUCCESS");
183 | return next();
184 | }
185 | });
186 | };
187 |
188 | influxController.getEndpointLogs = (req, res, next) => {
189 | const influxQuery = `
190 | from(bucket: "dev-bucket")
191 | |> range(start: -${range})
192 | |> filter(fn: (r) => r["_measurement"] == "monitoring")
193 | |> filter(fn: (r) => r["_field"] == "res_time" or r["_field"] == "status_code")
194 | |> filter(fn: (r) => r["method"] == "${req.query.method}")
195 | |> filter(fn: (r) => r["path"] == "${req.query.path}")
196 | `;
197 |
198 | // declare a logs object to collect labels and data
199 | const logs = {};
200 |
201 | // declare a stats object to collect labels and data
202 | queryApi.queryRows(influxQuery, {
203 | next(row, tableMeta) {
204 | const dataObject = tableMeta.toObject(row);
205 | if (logs[dataObject._time] === undefined) logs[dataObject._time] = {};
206 | logs[dataObject._time].timestamp = dataObject._time;
207 | logs[dataObject._time][dataObject._field] = dataObject._value;
208 | },
209 | error(error) {
210 | console.log("Query Finished ERROR");
211 | return next(error);
212 | },
213 | complete() {
214 | res.locals.logs = Object.values(logs);
215 | return next();
216 | }
217 | });
218 | };
219 |
220 | module.exports = influxController;
221 |
--------------------------------------------------------------------------------
/client/components/WorkspaceInfo.jsx:
--------------------------------------------------------------------------------
1 | import { PlayArrow, Stop, TimerOutlined } from "@mui/icons-material";
2 | import {
3 | Box,
4 | Button,
5 | ButtonGroup,
6 | Input, Typography
7 | } from "@mui/material";
8 | import React, { useState } from "react";
9 | import FlashError from "./FlashError.jsx";
10 |
11 | const WorkspaceInfo = (props) => {
12 | const {
13 | URIList,
14 | setURIList,
15 | workspaceId,
16 | name,
17 | domain,
18 | port,
19 | metricsPort,
20 | isMonitoring,
21 | setIsMonitoring,
22 | getURIListFromServer
23 | } = props;
24 | const [errorMessage, setErrorMessage] = useState("");
25 | const [searchInput, setSearchInput] = useState("");
26 | const [pingInterval, setPingInterval] = useState(1);
27 |
28 | const minPingInterval = 0.5;
29 |
30 | // const inputHandler = (e) => {
31 | // // * Convert input text to lower case
32 | // let lowerCase = e.target.value.toLowerCase();
33 | // setSearch(lowerCase);
34 | // };
35 |
36 | // const getURIListFromServer = () => {
37 | // fetch(
38 | // `http://localhost:${process.env.PORT}/routes/server?metrics_port=${metricsPort}`
39 | // )
40 | // .then((response) => response.json())
41 | // .then((data) => {
42 | // setURIList(data);
43 | // })
44 | // .catch((err) => {
45 | // setErrorMessage("Invalid server fetch request for the URI List");
46 | // // * reset the error message
47 | // setTimeout(() => setErrorMessage(""), 5000);
48 | // });
49 | // };
50 |
51 | // const getURIListFromDatabase = (workspace_id) => {
52 | // fetch(`http://localhost:${process.env.PORT}/routes/${workspace_id}`)
53 | // .then((response) => response.json())
54 | // .then((data) => {
55 | // setURIList(data);
56 | // })
57 | // .catch((err) => {
58 | // setErrorMessage("Invalid db fetch request for the URI List");
59 | // // * reset the error message
60 | // setTimeout(() => setErrorMessage(""), 5000);
61 | // });
62 | // };
63 |
64 | const handleStartMonitoringClick = (e) => {
65 | if (pingInterval === undefined || pingInterval < minPingInterval) return;
66 | e.preventDefault();
67 | fetch(`http://localhost:${process.env.PORT}/monitoring`, {
68 | method: "POST",
69 | headers: {
70 | "Content-Type": "application/json"
71 | },
72 | body: JSON.stringify({
73 | active: true,
74 | domain,
75 | interval: pingInterval,
76 | metricsPort,
77 | mode: "monitoring",
78 | port,
79 | verbose: true,
80 | workspaceId
81 | })
82 | })
83 | .then((serverResponse) => {
84 | if (serverResponse.ok) {
85 | setIsMonitoring(true);
86 | }
87 | })
88 | .catch((err) => {
89 | console.log("there was an error attempting to start monitoring: ", err);
90 | setErrorMessage(
91 | `Invalid POST request to start monitoring, error: ${err}`
92 | );
93 | });
94 | };
95 |
96 | const handleStopMonitoringClick = (e) => {
97 | e.preventDefault();
98 | setIsMonitoring(false);
99 | fetch(`http://localhost:${process.env.PORT}/monitoring`, {
100 | method: "POST",
101 | headers: {
102 | "Content-Type": "application/json"
103 | },
104 | body: JSON.stringify({
105 | active: false,
106 | verbose: true,
107 | workspaceId
108 | })
109 | })
110 | .then((serverResponse) => {
111 | if (serverResponse.ok) {
112 | setIsMonitoring(false);
113 | }
114 | })
115 | .catch((err) => {
116 | console.log("there was an error attempting to stop monitoring: ", err);
117 | setErrorMessage(
118 | `Invalid POST request to stop monitoring, error: ${err}`
119 | );
120 | });
121 | };
122 |
123 | return (
124 |
127 |
128 | {name}
129 |
130 |
131 | {domain}{((port !== undefined && typeof port === "number") ? ':' + port : '')}
132 |
133 | Monitoring Frequency
134 | }
150 | endAdornment={s}
151 | fullWidth={true}
152 | size="lg"
153 | sx={{ width: 70 }}
154 | onChange={(e) => {
155 | setPingInterval(e.target.value);
156 | }}
157 | />
158 |
159 |
160 |
174 |
188 |
189 | {/* */}
190 |
191 |
192 | {errorMessage !== "" ? (
193 |
194 | ) : null}
195 | {/*
*/}
213 | {/*
214 |
215 |
216 | | Tracking |
217 | Path |
218 | Method |
219 | Status Code |
220 | Simulate User Activity |
221 |
222 |
223 |
224 | {URIList.filter((uriObject) => {
225 | if (searchInput === "") {
226 | return uriObject;
227 | } else if (uriObject.path.toLowerCase() == searchInput) {
228 | return uriObject.path;
229 | }
230 | }).map((element) => {
231 | return (
232 |
241 | );
242 | })}
243 |
244 |
*/}
245 |
246 |
247 | );
248 | };
249 |
250 | export default WorkspaceInfo;
251 |
--------------------------------------------------------------------------------
/client/containers/NavBar.jsx:
--------------------------------------------------------------------------------
1 | // import React, { useState, useEffect } from "react";
2 | // import "../styles/NavBar.scss";
3 | // import HomeButton from "../components/HomeButton.jsx";
4 | // import { Link, NavLink, useNavigate } from "react-router-dom";
5 | // import Draggable from "react-draggable";
6 | // import { useTheme } from "@mui/material";
7 | // import { tokens } from "../theme.js";
8 |
9 | // const NavBar = (props) => {
10 | // const theme = useTheme();
11 | // const colors = tokens(theme.palette.mode);
12 | // const { setMainWidth, setMainOffset } = props;
13 | // const [workspaceList, setWorkspaceList] = useState([]);
14 |
15 | // const navigate = useNavigate();
16 |
17 | // const width = 300;
18 | // const height = "100vh";
19 | // const [xPosition, setX] = useState(-width);
20 |
21 | // const toggleMenu = () => {
22 | // if (xPosition < 0) {
23 | // getWorkSpaceList();
24 | // setX(0);
25 | // setMainWidth(`calc(100vw - ${width}px)`)
26 | // setMainOffset(`${width}px`)
27 | // } else {
28 | // setX(-width);
29 | // setMainWidth("100vw")
30 | // setMainOffset(`0px`)
31 | // }
32 | // };
33 |
34 | // useEffect(() => {
35 | // setX(-width);
36 | // }, []);
37 |
38 | // useEffect(() => {
39 | // getWorkSpaceList();
40 | // }, []);
41 |
42 | // const getWorkSpaceList = () => {
43 | // fetch(`http://localhost:${process.env.PORT}/workspaces`)
44 | // .then((response) => response.json())
45 | // .then((data) => {
46 | // setWorkspaceList(data);
47 | // })
48 | // .catch((err) => {
49 | // console.log(`there was an error: ${err}`);
50 | // });
51 | // };
52 |
53 | // return (
54 | //
55 | //
64 | //
65 | //
66 | //
73 | //
74 | //
75 | //
83 | //
84 | // {workspaceList.map((workspace) => {
85 | // const workspace_id = workspace._id;
86 | // const name = workspace.name;
87 | // const domain = workspace.domain;
88 | // return (
89 | //
90 | //
{
95 | // navigate(`/urilist/${workspace_id}`, {
96 | // state: { workspace_id, name, domain }
97 | // });
98 | // toggleMenu();
99 | // }}
100 | // >
101 | // {name}
102 | //
103 | //
104 | // );
105 | // })}
106 | //
107 | //
108 | //
109 | // );
110 | // };
111 |
112 | // export default NavBar;
113 |
114 | import React from "react";
115 | import "../styles/NavBar.scss";
116 |
117 | import { CssBaseline, ThemeProvider } from "@mui/material";
118 | import { ChevronLeft, Menu } from "@mui/icons-material";
119 | import {
120 | Divider,
121 | Drawer as MuiDrawer,
122 | IconButton,
123 | List,
124 | ListItem,
125 | ListItemButton,
126 | ListItemIcon,
127 | ListItemText
128 | } from "@mui/material";
129 | import { styled } from "@mui/material/styles";
130 | import { useTheme } from '@mui/material/styles'
131 | import { useMode } from '../theme.js'
132 |
133 | const SideBar = (props) => {
134 | const {open, handledrawerclose: handleDrawerClose } = props;
135 | // const theme = useTheme();
136 | const [theme] = useMode();
137 |
138 | const drawerWidth = 300;
139 | const openedMixin = (theme) => ({
140 | width: drawerWidth,
141 | transition: theme.transitions.create("width", {
142 | easing: theme.transitions.easing.sharp,
143 | duration: theme.transitions.duration.enteringScreen
144 | }),
145 | overflowX: "hidden"
146 | });
147 | const closedMixin = (theme) => ({
148 | transition: theme.transitions.create("width", {
149 | easing: theme.transitions.easing.sharp,
150 | duration: theme.transitions.duration.leavingScreen
151 | }),
152 | overflowX: "hidden",
153 | width: `calc(${theme.spacing(7)} + 1px)`,
154 | [theme.breakpoints.up("sm")]: {
155 | width: `calc(${theme.spacing(8)} + 1px)`
156 | }
157 | });
158 | const DrawerSection = styled("div")(({ theme }) => ({
159 | display: "flex",
160 | alignItems: "center",
161 | justifyContent: "flex-end",
162 | padding: theme.spacing(0, 1),
163 | // necessary for content to be below app bar
164 | ...theme.mixins.toolbar
165 | }));
166 | const Drawer = styled(MuiDrawer, {
167 | shouldForwardProp: (prop) => prop !== "open"
168 | })(({ theme, open }) => ({
169 | width: drawerWidth,
170 | flexShrink: 0,
171 | whiteSpace: "nowrap",
172 | boxSizing: "border-box",
173 | ...(open && {
174 | ...openedMixin(theme),
175 | "& .MuiDrawer-paper": openedMixin(theme)
176 | }),
177 | ...(!open && {
178 | ...closedMixin(theme),
179 | "& .MuiDrawer-paper": closedMixin(theme)
180 | })
181 | }));
182 |
183 | return (
184 | //
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
194 |
195 |
202 |
209 |
210 |
211 |
212 |
213 |
214 | ))}
215 |
216 |
217 |
218 | {["All mail", "Trash", "Spam"].map((text, index) => (
219 |
220 |
227 |
234 |
235 |
236 |
237 |
238 |
239 | ))}
240 |
241 |
242 | //
243 |
244 | );
245 | };
246 |
247 | export default SideBar;
248 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | require("dotenv").config({ path: path.resolve(__dirname, "../.env") });
3 | const express = require("express");
4 | const fetch = require("node-fetch");
5 | const cors = require("cors");
6 | const influxClient = require("./models/influx-client.js");
7 | const { Point } = require("@influxdata/influxdb-client");
8 | const chartRouter = require("./routes/chartdata");
9 | const logRouter = require("./routes/logRouter.js");
10 | const postgresClient = require("./models/postgres-client.js");
11 | const {
12 | PhoneNumberContext
13 | } = require("twilio/lib/rest/lookups/v1/phoneNumber.js");
14 | const pgController = require("./controllers/pgController.js");
15 | const { query } = require("express");
16 |
17 | const MODE = process.env.NODE_ENV || "production";
18 | const PORT = process.env.PORT || 9990;
19 |
20 | const app = express();
21 | app.use(express.json());
22 | app.use(cors());
23 | app.use(express.urlencoded({ extended: true }));
24 |
25 | if (MODE === "production") {
26 | app.use(express.static(path.join(__dirname, "../dist")));
27 | }
28 |
29 | // * Route all /chartdata requests to chartRouter
30 | app.use("/chartdata", chartRouter);
31 | app.use("/logdata", logRouter);
32 |
33 | let intervalId;
34 | let logs = [];
35 | let selectedEndpoints = [];
36 | let activeWorkspaceId;
37 | let monitoringStartTime, monitoringEndTime, timeElapsed;
38 | const trackedWorkspaces = {};
39 |
40 | const updateTimeElapsed = function () {
41 | monitoringEndTime = new Date();
42 | timeElapsed = new Date(monitoringEndTime - monitoringStartTime);
43 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s";
44 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s";
45 | };
46 |
47 | const timeSince = (startDate) => {
48 | const timeElapsed = new Date(new Date() - startDate)
49 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s";
50 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s";
51 | }
52 |
53 | const scrapeDataFromMetricsServer = async (metricsPort, tableName) => {
54 | try {
55 | logs = await (
56 | await fetch(`http://localhost:${metricsPort}/metrics`, {
57 | method: "DELETE"
58 | })
59 | ).json();
60 | console.log(`Storing to ${logs.length} entries to ${tableName}`)
61 | storeLogsToDatabase(logs, tableName);
62 | return logs;
63 | } catch (e) {
64 | console.error(e);
65 | return [];
66 | }
67 | };
68 |
69 | const storeLogsToDatabase = async (logsArr, tableName) => {
70 | try {
71 | const pointsArr = logsArr.map((log) => {
72 | return new Point(tableName)
73 | .tag("path", log.path)
74 | .tag("url", log.url)
75 | .tag("method", log.method)
76 | .floatField("res_time", log.response_time)
77 | .intField("status_code", log.status_code)
78 | .timestamp(new Date(log.date_created).getTime());
79 | });
80 | return influxClient.insertMultiple(pointsArr);
81 | } catch (e) {
82 | console.error(e);
83 | return false;
84 | }
85 | };
86 |
87 | const getTrackedEndpointsByWorkspaceId = async (workspaceId) => {
88 | const queryText = `
89 | SELECT *
90 | FROM endpoints
91 | WHERE
92 | workspace_id=${workspaceId} AND
93 | tracking=${true}
94 | ;`;
95 | const dbResponse = await postgresClient.query(queryText);
96 | return dbResponse.rows;
97 | }
98 |
99 | const pingEndpoints = async (domain, port, endpoints = []) => {
100 | const formattedPort = (port !== undefined && 0 < port && port < 9999) ? ':' + port : '';
101 | for (const endpoint of endpoints) {
102 | try {
103 | await fetch(`http://${domain}${formattedPort}${endpoint.path}`, {
104 | method: endpoint.method,
105 | headers: { "Cache-Control": "no-store" }
106 | });
107 | } catch (e) {
108 | console.error(e);
109 | }
110 | }
111 | };
112 |
113 | // endpoint to register user email and status codes to database
114 | app.post(
115 | "/registration",
116 | (req, res, next) => {
117 | let { subscribers, status300, status400, status500 } = req.body;
118 | try {
119 | const point = new Point("registration")
120 | .tag("email", subscribers)
121 | .booleanField("300", status300)
122 | .booleanField("400", status400)
123 | .booleanField("500", status500);
124 | influxClient.insertRegistration(point);
125 | return next();
126 | } catch (e) {
127 | console.error(e);
128 | }
129 | },
130 | (req, res) => res.sendStatus(200)
131 | );
132 |
133 | app.get("/monitoring/:workspaceId",
134 | (req, res) => {
135 | const {workspaceId} = req.params;
136 | return res.status(200).json(trackedWorkspaces[workspaceId]?.active || false)
137 | }
138 | );
139 |
140 | app.post("/monitoring", async (req, res) => {
141 | // * active is a boolean, interval is in seconds
142 | const { active, domain, metricsPort, mode, port, verbose, workspaceId } = req.body;
143 |
144 | if (active) {
145 | // * Enforce a minimum interval
146 | let interval = Math.max(0.5, req.body.interval);
147 | if (trackedWorkspaces[workspaceId] === undefined) trackedWorkspaces[workspaceId] = {};
148 | if (trackedWorkspaces[workspaceId].intervalId) clearInterval(intervalId);
149 | const start = new Date();
150 | const endpoints = await getTrackedEndpointsByWorkspaceId(workspaceId) || [];
151 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, {
152 | active,
153 | interval,
154 | intervalId: setInterval(() => {
155 | const elapsed = timeSince(trackedWorkspaces[workspaceId].start || new Date());
156 | trackedWorkspaces[workspaceId].elapsed = elapsed;
157 | if (verbose) {
158 | console.clear();
159 | console.log(`Monitoring for ${elapsed}`);
160 | }
161 | pingEndpoints(domain, port || '', endpoints);
162 | scrapeDataFromMetricsServer(metricsPort || 9991, `${mode}_${workspaceId}`);
163 | }, interval * 1000),
164 | domain,
165 | endpoints,
166 | metricsPort,
167 | mode,
168 | port,
169 | start,
170 | end: null,
171 | elapsed: 0,
172 | })
173 | }
174 |
175 | else {
176 | if (trackedWorkspaces[workspaceId]) {
177 | clearInterval(trackedWorkspaces[workspaceId]?.intervalId)
178 | trackedWorkspaces[workspaceId].active = false;
179 | }
180 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, {
181 | active,
182 | intervalId: null,
183 | endpoints: [],
184 | end: new Date(),
185 | })
186 | };
187 |
188 | if (verbose) {
189 | console.clear();
190 | console.log(`ACTIVE: ${active}`);
191 | }
192 |
193 | res.sendStatus(204);
194 | });
195 |
196 | const pingOneEndpoint = async (URI, method) => {
197 | console.log(`Sending traffic to: ${URI}`)
198 | try {
199 | await fetch(URI, {
200 | method: method,
201 | headers: {
202 | "Cache-Control": "no-cache"
203 | }
204 | });
205 | } catch (e) {
206 | console.error(e);
207 | }
208 | };
209 |
210 | const performRPS = async (domain, port, path, method, RPS) => {
211 | // console.log("PERFORMRPS");
212 | // console.table({
213 | // domain,
214 | // port,
215 | // path,
216 | // method,
217 | // RPS,
218 | // })
219 | // console.log("PERFORMRPS URI\nhttp://" + domain + (typeof port === "number") ? port : '' + path);
220 | const interval = Math.floor(1000 / RPS);
221 | if (intervalId) clearInterval(intervalId);
222 | let counter = 0;
223 | intervalId = setInterval(() => {
224 | console.clear()
225 | console.log(++counter);
226 | pingOneEndpoint(`http://${domain}${(typeof port === "number") ? ':' + port : ''}${path}`, method);
227 | }, interval);
228 | };
229 |
230 | const rpswithInterval = async (domain, port, path, method, RPS, timeInterval) => {
231 | if (intervalId) clearInterval(intervalId);
232 | intervalId = setInterval(() => {
233 | performRPS(domain, port, path, method, RPS);
234 | console.log("PING FINISHED");
235 | }, timeInterval * 1000);
236 | };
237 |
238 | app.post("/simulation", async (req, res) => {
239 | const { workspaceId, domain, port, path, method, metricsPort, RPS, timeInterval, setTime, stop } = req.body;
240 | if (!stop) {
241 | rpswithInterval(domain, port, path, method, RPS, timeInterval);
242 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`);
243 | } else {
244 | clearInterval(intervalId)
245 | console.log("Scraping...");
246 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`)
247 | };
248 | console.log("PING RESULT DONE");
249 | return res.sendStatus(200);
250 | });
251 |
252 | app.get("/metrics", async (req, res) => {
253 | return res.status(200).json(logs);
254 | });
255 |
256 | app.put("/endpoints/:_id",
257 | pgController.updateEndpointById,
258 | (req, res) => {
259 | return res.sendStatus(204);
260 | }
261 | )
262 |
263 | app.put("/endpoints2/",
264 | pgController.updateEndpointByRoute,
265 | (req, res) => {
266 | return res.sendStatus(204);
267 | }
268 | )
269 |
270 | app.put("/routes/server", async (req, res, next) => {
271 | const { workspaceId, metricsPort } = req.body;
272 | try {
273 | res.locals.workspaceId = workspaceId;
274 | const response = await fetch(`http://localhost:${metricsPort}/endpoints`)
275 | const routes = await response.json();
276 | let queryText = `
277 | DELETE
278 | FROM endpoints
279 | WHERE workspace_id=${workspaceId}
280 | ;`;
281 | routes.forEach((route) => {
282 | // route.status = 200;
283 | route.tracking = false;
284 | queryText += `
285 | INSERT INTO endpoints (method, path, tracking, workspace_id)
286 | VALUES ('${route.method}', '${route.path}', ${route.tracking}, ${workspaceId})
287 | ON CONFLICT ON CONSTRAINT endpoints_uq
288 | DO UPDATE SET tracking = ${route.tracking};
289 | `;
290 | });
291 | await postgresClient.query(queryText);
292 | }
293 | catch (err) {
294 | return console.error(err);
295 | }
296 | let dbResponse = [];
297 | try {
298 | queryText = `
299 | SELECT *
300 | FROM endpoints
301 | WHERE workspace_id=${workspaceId}
302 | ;`;
303 | dbResponse = (await postgresClient.query(queryText)).rows;
304 | }
305 | catch (err) {
306 | console.error(err);
307 | return next(err);
308 | }
309 | return res.status(200).json(dbResponse);
310 | });
311 |
312 | app.get("/routes/:workspace_id", async (req, res) => {
313 | const { workspace_id } = req.params;
314 | const queryText = `
315 | SELECT *
316 | FROM endpoints
317 | WHERE workspace_id = $1;`;
318 | const dbResponse = await postgresClient.query(queryText, [workspace_id]);
319 | return res.status(200).json(dbResponse.rows);
320 | });
321 |
322 | app.post("/routes/:workspace_id", async (req, res) => {
323 | const { workspace_id } = req.params;
324 | let queryText = "";
325 | req.body.forEach((URI) => {
326 | queryText += `
327 | INSERT INTO endpoints (method, path, tracking, workspace_id)
328 | VALUES ('${URI.method}', '${URI.path}', ${URI.tracking}, ${workspace_id})
329 | ON CONFLICT ON CONSTRAINT endpoints_uq
330 | DO UPDATE SET tracking = ${URI.tracking};`;
331 | });
332 | postgresClient.query(queryText);
333 | selectedEndpoints = req.body.filter((URI) => URI.tracking) || req.body;
334 | return res.sendStatus(204);
335 | });
336 |
337 | // get existing workspaces for the user
338 | app.get("/workspaces", async (req, res) => {
339 | const queryText = `
340 | SELECT *
341 | FROM workspaces
342 | ;`;
343 | const dbResponse = await postgresClient.query(queryText);
344 | return res.status(200).json(dbResponse.rows);
345 | });
346 |
347 | // create a new workspace for the user
348 | app.post("/workspaces", async (req, res) => {
349 | const { name, domain, port, metricsPort } = req.body;
350 | let queryText = `
351 | INSERT INTO workspaces (name, domain, port, metrics_port)
352 | VALUES ($1, $2, $3, $4)
353 | ;`;
354 | postgresClient.query(queryText, [name, domain, port, metricsPort]);
355 | return res.sendStatus(204);
356 | });
357 |
358 | app.delete("/workspaces", async (req, res) => {
359 | const { workspace_id } = req.body;
360 | const queryText = `
361 | DELETE FROM workspaces
362 | WHERE _id=${workspace_id}
363 | ;`;
364 | await postgresClient.query(queryText);
365 | return res.sendStatus(204);
366 | });
367 |
368 | app.delete("/endpoints/:workspaceId",
369 | pgController.deleteEndpointsByWorkspaceId,
370 | async (req, res) => {
371 | return res.sendStatus(204);
372 | }
373 | );
374 |
375 | app.listen(PORT, () => {
376 | console.log(
377 | `Application server started on port ${PORT}\n${MODE.toUpperCase()} mode`
378 | );
379 | });
380 |
--------------------------------------------------------------------------------
/client/components/URITable.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { alpha } from "@mui/material/styles";
4 | import {
5 | Box,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableContainer,
10 | TableHead,
11 | TablePagination,
12 | TableRow,
13 | TableSortLabel,
14 | Toolbar,
15 | Typography,
16 | Paper,
17 | Checkbox,
18 | IconButton,
19 | Tooltip,
20 | FormControlLabel,
21 | Switch
22 | } from "@mui/material";
23 | import { Delete, FilterList, Sync as Refresh } from "@mui/icons-material";
24 | import { visuallyHidden } from "@mui/utils";
25 | import { useNavigate } from "react-router-dom";
26 | import SearchBar from "./SearchBar.jsx"
27 |
28 | function descendingComparator(a, b, orderBy) {
29 | if (b[orderBy] < a[orderBy]) {
30 | return -1;
31 | }
32 | if (b[orderBy] > a[orderBy]) {
33 | return 1;
34 | }
35 | return 0;
36 | }
37 |
38 | function getComparator(order, orderBy) {
39 | return order === "desc"
40 | ? (a, b) => descendingComparator(a, b, orderBy)
41 | : (a, b) => -descendingComparator(a, b, orderBy);
42 | }
43 |
44 | // Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
45 | // stableSort() brings sort stability to non-modern browsers (notably IE11). If you
46 | // only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
47 | // with exampleArray.slice().sort(exampleComparator)
48 | function stableSort(array, comparator) {
49 | const stabilizedThis = array.map((el, index) => [el, index]);
50 | stabilizedThis.sort((a, b) => {
51 | const order = comparator(a[0], b[0]);
52 | if (order !== 0) {
53 | return order;
54 | }
55 | return a[1] - b[1];
56 | });
57 | return stabilizedThis.map((el) => el[0]);
58 | }
59 |
60 | const generateHeadCells = (rows) => {
61 | if (! rows?.length > 0) return [];
62 | return (Object.keys(rows[0]))
63 | .filter(key => key[0] !== '_')
64 | .map((key) => {
65 | return {
66 | id: key,
67 | numeric: typeof rows[key] === "number",
68 | disablePadding: true,
69 | label: key
70 | .replaceAll("_", " ")
71 | .split(" ")
72 | .map((word) => (word[0] ? word[0] : '').toUpperCase() + word.slice(1))
73 | .join(" ")
74 | };
75 | }
76 | );
77 | };
78 |
79 | function DataTableHead(props) {
80 |
81 | const {
82 | headCells,
83 | onSelectAllClick,
84 | order,
85 | orderBy,
86 | numSelected,
87 | rowCount,
88 | onRequestSort
89 | } = props;
90 | const createSortHandler = (property) => (event) => {
91 | onRequestSort(event, property);
92 | };
93 |
94 | return (
95 |
96 |
97 |
98 | 0 && numSelected < rowCount}
101 | checked={rowCount > 0 && numSelected === rowCount}
102 | onChange={onSelectAllClick}
103 | inputProps={{
104 | "aria-label": "select all desserts"
105 | }}
106 | />
107 |
108 | {headCells.map((headCell) => (
109 |
116 |
121 | {headCell.label}
122 | {orderBy === headCell.id ? (
123 |
124 | {order === "desc" ? "sorted descending" : "sorted ascending"}
125 |
126 | ) : null}
127 |
128 |
129 | ))}
130 |
131 |
132 | );
133 | }
134 |
135 | DataTableHead.propTypes = {
136 | numSelected: PropTypes.number.isRequired,
137 | onRequestSort: PropTypes.func.isRequired,
138 | onSelectAllClick: PropTypes.func.isRequired,
139 | order: PropTypes.oneOf(["asc", "desc"]).isRequired,
140 | orderBy: PropTypes.string.isRequired,
141 | rowCount: PropTypes.number.isRequired
142 | };
143 |
144 | function DataTableToolbar(props) {
145 | const { numSelected, metricsPort, refreshURIList, workspaceId, searchQuery, setSearchQuery, handleSearchChange } = props;
146 |
147 | return (
148 | 0 && {
153 | bgcolor: (theme) =>
154 | alpha(
155 | theme.palette.primary.main,
156 | theme.palette.action.activatedOpacity
157 | )
158 | })
159 | }}
160 | >
161 | {numSelected > 0 ? (
162 |
168 | {numSelected} selected
169 |
170 | ) : (
171 |
179 |
186 |
191 | Endpoints
192 |
193 |
194 |
199 |
204 |
205 |
212 |
213 | {
215 | // getURIListFromServer(props.metricsPort)
216 | refreshURIList(workspaceId, metricsPort);
217 | }}
218 | >
219 |
220 |
221 |
222 |
223 |
224 | )}
225 | {numSelected > 0 ? (
226 |
227 |
228 |
229 |
230 |
231 | ) : (
232 | <>
233 |
234 | >
235 | )}
236 |
237 |
238 | );
239 | }
240 |
241 | DataTableToolbar.propTypes = {
242 | numSelected: PropTypes.number.isRequired
243 | };
244 |
245 | export default function URITable(props) {
246 |
247 | const {
248 | workspaceId,
249 | name,
250 | domain,
251 | port,
252 | metricsPort,
253 | rows,
254 | getURIListFromServer,
255 | updateTrackingInDatabaseById,
256 | refreshURIList,
257 | isMonitoring,
258 | setIsMonitoring
259 | } = props;
260 |
261 | const headCells = generateHeadCells(rows);
262 |
263 | const [searchQuery, setSearchQuery] = useState("")
264 |
265 | const handleSearchChange = (event) => {
266 | setSearchQuery(event.target.value);
267 | }
268 |
269 | const filterBySearchQuery = (unfilteredRows, searchQuery = "") => {
270 | return unfilteredRows.filter((row) => {
271 | return (
272 | Object.keys(row || {})
273 | .filter(columnName => columnName[0] !== '_')
274 | .some(column => row[column].toString().toLowerCase().includes(searchQuery.toLowerCase()))
275 | )
276 | })
277 | }
278 |
279 | const navigate = useNavigate();
280 |
281 | const [order, setOrder] = React.useState("asc");
282 | const [orderBy, setOrderBy] = React.useState("path");
283 | const [selected, setSelected] = React.useState(rows.filter((row) => row.tracking));
284 | const [page, setPage] = React.useState(0);
285 | const [dense, setDense] = React.useState(true);
286 | const [rowsPerPage, setRowsPerPage] = React.useState(10);
287 |
288 | const handleRequestSort = (event, property) => {
289 | const isAsc = orderBy === property && order === "asc";
290 | setOrder(isAsc ? "desc" : "asc");
291 | setOrderBy(property);
292 | };
293 |
294 | const handleSelectAllClick = (event) => {
295 | if (event.target.checked) {
296 | const newSelected = rows.map((n) => n.name);
297 | setSelected(newSelected);
298 | return;
299 | }
300 | setSelected([]);
301 | };
302 |
303 | const handleClick = (event, identifier) => {
304 | const selectedIndex = selected.indexOf(identifier);
305 | let newSelected = [];
306 |
307 | if (selectedIndex === -1) {
308 | newSelected = newSelected.concat(selected, identifier);
309 | } else if (selectedIndex === 0) {
310 | newSelected = newSelected.concat(selected.slice(1));
311 | } else if (selectedIndex === selected.length - 1) {
312 | newSelected = newSelected.concat(selected.slice(0, -1));
313 | } else if (selectedIndex > 0) {
314 | newSelected = newSelected.concat(
315 | selected.slice(0, selectedIndex),
316 | selected.slice(selectedIndex + 1)
317 | );
318 | }
319 |
320 | setSelected(newSelected);
321 | };
322 |
323 | const handleChangePage = (event, newPage) => {
324 | setPage(newPage);
325 | };
326 |
327 | const handleChangeRowsPerPage = (event) => {
328 | setRowsPerPage(parseInt(event.target.value, 10));
329 | setPage(0);
330 | };
331 |
332 | const handleChangeDense = (event) => {
333 | setDense(event.target.checked);
334 | };
335 |
336 | const isSelected = (identifier) => selected.indexOf(identifier) !== -1;
337 |
338 | // Avoid a layout jump when reaching the last page with empty rows.
339 | const emptyRows =
340 | page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
341 |
342 | return (
343 |
344 |
345 |
355 |
356 |
361 |
370 |
371 | {filterBySearchQuery(stableSort(rows, getComparator(order, orderBy)), searchQuery)
372 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
373 | .map((row, index) => {
374 | const isItemSelected = isSelected(row.id);
375 | const labelId = `enhanced-table-checkbox-${index}`;
376 |
377 | return (
378 | handleClick(event, row.name)}
381 | role="checkbox"
382 | aria-checked={isItemSelected}
383 | tabIndex={-1}
384 | key={crypto.randomUUID()}
385 | selected={isItemSelected}
386 | >
387 |
388 | {
396 | row.tracking = ! row._tracking
397 | const newRow = Object.assign({}, {
398 | method: row.method,
399 | path: row.path,
400 | tracking: row.tracking,
401 | _id: row._id,
402 | })
403 | try {
404 | updateTrackingInDatabaseById(newRow)
405 | } catch (err1) {
406 | console.error(err1);
407 | try {
408 | updateTrackingInDatabaseByRoute(newRow)
409 | }
410 | catch (err2) {
411 | console.error(err2);
412 | }
413 | }
414 | }}
415 | />
416 |
417 |
418 | {Object.keys(row)
419 | .filter(key => key[0] !== '_')
420 | .map((column) => {
421 | return (
422 | {
426 | if (column === "simulation" || column === "open") return;
427 | navigate(`/dashboard/${row._id}`, { state: {
428 | workspaceId,
429 | name,
430 | domain,
431 | port,
432 | metricsPort,
433 | endpointId: row._id,
434 | method: row.method,
435 | path: row.path,
436 | isMonitoring,
437 | }})
438 | }}
439 | sx={{ cursor: "pointer" }}
440 | >
441 | {row[column]}
442 |
443 | );
444 | })
445 | }
446 |
447 |
448 | );
449 | })}
450 | {emptyRows > 0 && (
451 |
456 |
457 |
458 | )}
459 |
460 |
461 |
462 |
471 |
472 | {/* }
474 | label="Compact view"
475 | /> */}
476 |
477 | );
478 | }
479 |
--------------------------------------------------------------------------------
/dummy/module/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-endpoints-monitor",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "express-endpoints-monitor",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "express": "^4.18.2",
13 | "express-list-endpoints": "^6.0.0",
14 | "response-time": "^2.3.2"
15 | }
16 | },
17 | "node_modules/accepts": {
18 | "version": "1.3.8",
19 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
20 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
21 | "dependencies": {
22 | "mime-types": "~2.1.34",
23 | "negotiator": "0.6.3"
24 | },
25 | "engines": {
26 | "node": ">= 0.6"
27 | }
28 | },
29 | "node_modules/array-flatten": {
30 | "version": "1.1.1",
31 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
32 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
33 | },
34 | "node_modules/body-parser": {
35 | "version": "1.20.1",
36 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
37 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
38 | "dependencies": {
39 | "bytes": "3.1.2",
40 | "content-type": "~1.0.4",
41 | "debug": "2.6.9",
42 | "depd": "2.0.0",
43 | "destroy": "1.2.0",
44 | "http-errors": "2.0.0",
45 | "iconv-lite": "0.4.24",
46 | "on-finished": "2.4.1",
47 | "qs": "6.11.0",
48 | "raw-body": "2.5.1",
49 | "type-is": "~1.6.18",
50 | "unpipe": "1.0.0"
51 | },
52 | "engines": {
53 | "node": ">= 0.8",
54 | "npm": "1.2.8000 || >= 1.4.16"
55 | }
56 | },
57 | "node_modules/bytes": {
58 | "version": "3.1.2",
59 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
60 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
61 | "engines": {
62 | "node": ">= 0.8"
63 | }
64 | },
65 | "node_modules/call-bind": {
66 | "version": "1.0.2",
67 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
68 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
69 | "dependencies": {
70 | "function-bind": "^1.1.1",
71 | "get-intrinsic": "^1.0.2"
72 | },
73 | "funding": {
74 | "url": "https://github.com/sponsors/ljharb"
75 | }
76 | },
77 | "node_modules/content-disposition": {
78 | "version": "0.5.4",
79 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
80 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
81 | "dependencies": {
82 | "safe-buffer": "5.2.1"
83 | },
84 | "engines": {
85 | "node": ">= 0.6"
86 | }
87 | },
88 | "node_modules/content-type": {
89 | "version": "1.0.4",
90 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
91 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
92 | "engines": {
93 | "node": ">= 0.6"
94 | }
95 | },
96 | "node_modules/cookie": {
97 | "version": "0.5.0",
98 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
99 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
100 | "engines": {
101 | "node": ">= 0.6"
102 | }
103 | },
104 | "node_modules/cookie-signature": {
105 | "version": "1.0.6",
106 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
107 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
108 | },
109 | "node_modules/debug": {
110 | "version": "2.6.9",
111 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
112 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
113 | "dependencies": {
114 | "ms": "2.0.0"
115 | }
116 | },
117 | "node_modules/depd": {
118 | "version": "2.0.0",
119 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
120 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
121 | "engines": {
122 | "node": ">= 0.8"
123 | }
124 | },
125 | "node_modules/destroy": {
126 | "version": "1.2.0",
127 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
128 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
129 | "engines": {
130 | "node": ">= 0.8",
131 | "npm": "1.2.8000 || >= 1.4.16"
132 | }
133 | },
134 | "node_modules/ee-first": {
135 | "version": "1.1.1",
136 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
137 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
138 | },
139 | "node_modules/encodeurl": {
140 | "version": "1.0.2",
141 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
142 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
143 | "engines": {
144 | "node": ">= 0.8"
145 | }
146 | },
147 | "node_modules/escape-html": {
148 | "version": "1.0.3",
149 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
150 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
151 | },
152 | "node_modules/etag": {
153 | "version": "1.8.1",
154 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
155 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
156 | "engines": {
157 | "node": ">= 0.6"
158 | }
159 | },
160 | "node_modules/express": {
161 | "version": "4.18.2",
162 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
163 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
164 | "dependencies": {
165 | "accepts": "~1.3.8",
166 | "array-flatten": "1.1.1",
167 | "body-parser": "1.20.1",
168 | "content-disposition": "0.5.4",
169 | "content-type": "~1.0.4",
170 | "cookie": "0.5.0",
171 | "cookie-signature": "1.0.6",
172 | "debug": "2.6.9",
173 | "depd": "2.0.0",
174 | "encodeurl": "~1.0.2",
175 | "escape-html": "~1.0.3",
176 | "etag": "~1.8.1",
177 | "finalhandler": "1.2.0",
178 | "fresh": "0.5.2",
179 | "http-errors": "2.0.0",
180 | "merge-descriptors": "1.0.1",
181 | "methods": "~1.1.2",
182 | "on-finished": "2.4.1",
183 | "parseurl": "~1.3.3",
184 | "path-to-regexp": "0.1.7",
185 | "proxy-addr": "~2.0.7",
186 | "qs": "6.11.0",
187 | "range-parser": "~1.2.1",
188 | "safe-buffer": "5.2.1",
189 | "send": "0.18.0",
190 | "serve-static": "1.15.0",
191 | "setprototypeof": "1.2.0",
192 | "statuses": "2.0.1",
193 | "type-is": "~1.6.18",
194 | "utils-merge": "1.0.1",
195 | "vary": "~1.1.2"
196 | },
197 | "engines": {
198 | "node": ">= 0.10.0"
199 | }
200 | },
201 | "node_modules/express-list-endpoints": {
202 | "version": "6.0.0",
203 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz",
204 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg==",
205 | "engines": {
206 | "node": ">=10"
207 | }
208 | },
209 | "node_modules/finalhandler": {
210 | "version": "1.2.0",
211 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
212 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
213 | "dependencies": {
214 | "debug": "2.6.9",
215 | "encodeurl": "~1.0.2",
216 | "escape-html": "~1.0.3",
217 | "on-finished": "2.4.1",
218 | "parseurl": "~1.3.3",
219 | "statuses": "2.0.1",
220 | "unpipe": "~1.0.0"
221 | },
222 | "engines": {
223 | "node": ">= 0.8"
224 | }
225 | },
226 | "node_modules/forwarded": {
227 | "version": "0.2.0",
228 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
229 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
230 | "engines": {
231 | "node": ">= 0.6"
232 | }
233 | },
234 | "node_modules/fresh": {
235 | "version": "0.5.2",
236 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
237 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
238 | "engines": {
239 | "node": ">= 0.6"
240 | }
241 | },
242 | "node_modules/function-bind": {
243 | "version": "1.1.1",
244 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
245 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
246 | },
247 | "node_modules/get-intrinsic": {
248 | "version": "1.1.3",
249 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
250 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
251 | "dependencies": {
252 | "function-bind": "^1.1.1",
253 | "has": "^1.0.3",
254 | "has-symbols": "^1.0.3"
255 | },
256 | "funding": {
257 | "url": "https://github.com/sponsors/ljharb"
258 | }
259 | },
260 | "node_modules/has": {
261 | "version": "1.0.3",
262 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
263 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
264 | "dependencies": {
265 | "function-bind": "^1.1.1"
266 | },
267 | "engines": {
268 | "node": ">= 0.4.0"
269 | }
270 | },
271 | "node_modules/has-symbols": {
272 | "version": "1.0.3",
273 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
274 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
275 | "engines": {
276 | "node": ">= 0.4"
277 | },
278 | "funding": {
279 | "url": "https://github.com/sponsors/ljharb"
280 | }
281 | },
282 | "node_modules/http-errors": {
283 | "version": "2.0.0",
284 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
285 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
286 | "dependencies": {
287 | "depd": "2.0.0",
288 | "inherits": "2.0.4",
289 | "setprototypeof": "1.2.0",
290 | "statuses": "2.0.1",
291 | "toidentifier": "1.0.1"
292 | },
293 | "engines": {
294 | "node": ">= 0.8"
295 | }
296 | },
297 | "node_modules/iconv-lite": {
298 | "version": "0.4.24",
299 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
300 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
301 | "dependencies": {
302 | "safer-buffer": ">= 2.1.2 < 3"
303 | },
304 | "engines": {
305 | "node": ">=0.10.0"
306 | }
307 | },
308 | "node_modules/inherits": {
309 | "version": "2.0.4",
310 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
311 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
312 | },
313 | "node_modules/ipaddr.js": {
314 | "version": "1.9.1",
315 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
316 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
317 | "engines": {
318 | "node": ">= 0.10"
319 | }
320 | },
321 | "node_modules/media-typer": {
322 | "version": "0.3.0",
323 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
324 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
325 | "engines": {
326 | "node": ">= 0.6"
327 | }
328 | },
329 | "node_modules/merge-descriptors": {
330 | "version": "1.0.1",
331 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
332 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
333 | },
334 | "node_modules/methods": {
335 | "version": "1.1.2",
336 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
337 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
338 | "engines": {
339 | "node": ">= 0.6"
340 | }
341 | },
342 | "node_modules/mime": {
343 | "version": "1.6.0",
344 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
345 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
346 | "bin": {
347 | "mime": "cli.js"
348 | },
349 | "engines": {
350 | "node": ">=4"
351 | }
352 | },
353 | "node_modules/mime-db": {
354 | "version": "1.52.0",
355 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
356 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
357 | "engines": {
358 | "node": ">= 0.6"
359 | }
360 | },
361 | "node_modules/mime-types": {
362 | "version": "2.1.35",
363 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
364 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
365 | "dependencies": {
366 | "mime-db": "1.52.0"
367 | },
368 | "engines": {
369 | "node": ">= 0.6"
370 | }
371 | },
372 | "node_modules/ms": {
373 | "version": "2.0.0",
374 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
375 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
376 | },
377 | "node_modules/negotiator": {
378 | "version": "0.6.3",
379 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
380 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
381 | "engines": {
382 | "node": ">= 0.6"
383 | }
384 | },
385 | "node_modules/object-inspect": {
386 | "version": "1.12.2",
387 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
388 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
389 | "funding": {
390 | "url": "https://github.com/sponsors/ljharb"
391 | }
392 | },
393 | "node_modules/on-finished": {
394 | "version": "2.4.1",
395 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
396 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
397 | "dependencies": {
398 | "ee-first": "1.1.1"
399 | },
400 | "engines": {
401 | "node": ">= 0.8"
402 | }
403 | },
404 | "node_modules/on-headers": {
405 | "version": "1.0.2",
406 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
407 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
408 | "engines": {
409 | "node": ">= 0.8"
410 | }
411 | },
412 | "node_modules/parseurl": {
413 | "version": "1.3.3",
414 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
415 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
416 | "engines": {
417 | "node": ">= 0.8"
418 | }
419 | },
420 | "node_modules/path-to-regexp": {
421 | "version": "0.1.7",
422 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
423 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
424 | },
425 | "node_modules/proxy-addr": {
426 | "version": "2.0.7",
427 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
428 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
429 | "dependencies": {
430 | "forwarded": "0.2.0",
431 | "ipaddr.js": "1.9.1"
432 | },
433 | "engines": {
434 | "node": ">= 0.10"
435 | }
436 | },
437 | "node_modules/qs": {
438 | "version": "6.11.0",
439 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
440 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
441 | "dependencies": {
442 | "side-channel": "^1.0.4"
443 | },
444 | "engines": {
445 | "node": ">=0.6"
446 | },
447 | "funding": {
448 | "url": "https://github.com/sponsors/ljharb"
449 | }
450 | },
451 | "node_modules/range-parser": {
452 | "version": "1.2.1",
453 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
454 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
455 | "engines": {
456 | "node": ">= 0.6"
457 | }
458 | },
459 | "node_modules/raw-body": {
460 | "version": "2.5.1",
461 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
462 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
463 | "dependencies": {
464 | "bytes": "3.1.2",
465 | "http-errors": "2.0.0",
466 | "iconv-lite": "0.4.24",
467 | "unpipe": "1.0.0"
468 | },
469 | "engines": {
470 | "node": ">= 0.8"
471 | }
472 | },
473 | "node_modules/response-time": {
474 | "version": "2.3.2",
475 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz",
476 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==",
477 | "dependencies": {
478 | "depd": "~1.1.0",
479 | "on-headers": "~1.0.1"
480 | },
481 | "engines": {
482 | "node": ">= 0.8.0"
483 | }
484 | },
485 | "node_modules/response-time/node_modules/depd": {
486 | "version": "1.1.2",
487 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
488 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
489 | "engines": {
490 | "node": ">= 0.6"
491 | }
492 | },
493 | "node_modules/safe-buffer": {
494 | "version": "5.2.1",
495 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
496 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
497 | "funding": [
498 | {
499 | "type": "github",
500 | "url": "https://github.com/sponsors/feross"
501 | },
502 | {
503 | "type": "patreon",
504 | "url": "https://www.patreon.com/feross"
505 | },
506 | {
507 | "type": "consulting",
508 | "url": "https://feross.org/support"
509 | }
510 | ]
511 | },
512 | "node_modules/safer-buffer": {
513 | "version": "2.1.2",
514 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
515 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
516 | },
517 | "node_modules/send": {
518 | "version": "0.18.0",
519 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
520 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
521 | "dependencies": {
522 | "debug": "2.6.9",
523 | "depd": "2.0.0",
524 | "destroy": "1.2.0",
525 | "encodeurl": "~1.0.2",
526 | "escape-html": "~1.0.3",
527 | "etag": "~1.8.1",
528 | "fresh": "0.5.2",
529 | "http-errors": "2.0.0",
530 | "mime": "1.6.0",
531 | "ms": "2.1.3",
532 | "on-finished": "2.4.1",
533 | "range-parser": "~1.2.1",
534 | "statuses": "2.0.1"
535 | },
536 | "engines": {
537 | "node": ">= 0.8.0"
538 | }
539 | },
540 | "node_modules/send/node_modules/ms": {
541 | "version": "2.1.3",
542 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
543 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
544 | },
545 | "node_modules/serve-static": {
546 | "version": "1.15.0",
547 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
548 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
549 | "dependencies": {
550 | "encodeurl": "~1.0.2",
551 | "escape-html": "~1.0.3",
552 | "parseurl": "~1.3.3",
553 | "send": "0.18.0"
554 | },
555 | "engines": {
556 | "node": ">= 0.8.0"
557 | }
558 | },
559 | "node_modules/setprototypeof": {
560 | "version": "1.2.0",
561 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
562 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
563 | },
564 | "node_modules/side-channel": {
565 | "version": "1.0.4",
566 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
567 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
568 | "dependencies": {
569 | "call-bind": "^1.0.0",
570 | "get-intrinsic": "^1.0.2",
571 | "object-inspect": "^1.9.0"
572 | },
573 | "funding": {
574 | "url": "https://github.com/sponsors/ljharb"
575 | }
576 | },
577 | "node_modules/statuses": {
578 | "version": "2.0.1",
579 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
580 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
581 | "engines": {
582 | "node": ">= 0.8"
583 | }
584 | },
585 | "node_modules/toidentifier": {
586 | "version": "1.0.1",
587 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
588 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
589 | "engines": {
590 | "node": ">=0.6"
591 | }
592 | },
593 | "node_modules/type-is": {
594 | "version": "1.6.18",
595 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
596 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
597 | "dependencies": {
598 | "media-typer": "0.3.0",
599 | "mime-types": "~2.1.24"
600 | },
601 | "engines": {
602 | "node": ">= 0.6"
603 | }
604 | },
605 | "node_modules/unpipe": {
606 | "version": "1.0.0",
607 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
608 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
609 | "engines": {
610 | "node": ">= 0.8"
611 | }
612 | },
613 | "node_modules/utils-merge": {
614 | "version": "1.0.1",
615 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
616 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
617 | "engines": {
618 | "node": ">= 0.4.0"
619 | }
620 | },
621 | "node_modules/vary": {
622 | "version": "1.1.2",
623 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
624 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
625 | "engines": {
626 | "node": ">= 0.8"
627 | }
628 | }
629 | },
630 | "dependencies": {
631 | "accepts": {
632 | "version": "1.3.8",
633 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
634 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
635 | "requires": {
636 | "mime-types": "~2.1.34",
637 | "negotiator": "0.6.3"
638 | }
639 | },
640 | "array-flatten": {
641 | "version": "1.1.1",
642 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
643 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
644 | },
645 | "body-parser": {
646 | "version": "1.20.1",
647 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
648 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
649 | "requires": {
650 | "bytes": "3.1.2",
651 | "content-type": "~1.0.4",
652 | "debug": "2.6.9",
653 | "depd": "2.0.0",
654 | "destroy": "1.2.0",
655 | "http-errors": "2.0.0",
656 | "iconv-lite": "0.4.24",
657 | "on-finished": "2.4.1",
658 | "qs": "6.11.0",
659 | "raw-body": "2.5.1",
660 | "type-is": "~1.6.18",
661 | "unpipe": "1.0.0"
662 | }
663 | },
664 | "bytes": {
665 | "version": "3.1.2",
666 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
667 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
668 | },
669 | "call-bind": {
670 | "version": "1.0.2",
671 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
672 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
673 | "requires": {
674 | "function-bind": "^1.1.1",
675 | "get-intrinsic": "^1.0.2"
676 | }
677 | },
678 | "content-disposition": {
679 | "version": "0.5.4",
680 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
681 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
682 | "requires": {
683 | "safe-buffer": "5.2.1"
684 | }
685 | },
686 | "content-type": {
687 | "version": "1.0.4",
688 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
689 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
690 | },
691 | "cookie": {
692 | "version": "0.5.0",
693 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
694 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
695 | },
696 | "cookie-signature": {
697 | "version": "1.0.6",
698 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
699 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
700 | },
701 | "debug": {
702 | "version": "2.6.9",
703 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
704 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
705 | "requires": {
706 | "ms": "2.0.0"
707 | }
708 | },
709 | "depd": {
710 | "version": "2.0.0",
711 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
712 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
713 | },
714 | "destroy": {
715 | "version": "1.2.0",
716 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
717 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
718 | },
719 | "ee-first": {
720 | "version": "1.1.1",
721 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
722 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
723 | },
724 | "encodeurl": {
725 | "version": "1.0.2",
726 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
727 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
728 | },
729 | "escape-html": {
730 | "version": "1.0.3",
731 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
732 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
733 | },
734 | "etag": {
735 | "version": "1.8.1",
736 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
737 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
738 | },
739 | "express": {
740 | "version": "4.18.2",
741 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
742 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
743 | "requires": {
744 | "accepts": "~1.3.8",
745 | "array-flatten": "1.1.1",
746 | "body-parser": "1.20.1",
747 | "content-disposition": "0.5.4",
748 | "content-type": "~1.0.4",
749 | "cookie": "0.5.0",
750 | "cookie-signature": "1.0.6",
751 | "debug": "2.6.9",
752 | "depd": "2.0.0",
753 | "encodeurl": "~1.0.2",
754 | "escape-html": "~1.0.3",
755 | "etag": "~1.8.1",
756 | "finalhandler": "1.2.0",
757 | "fresh": "0.5.2",
758 | "http-errors": "2.0.0",
759 | "merge-descriptors": "1.0.1",
760 | "methods": "~1.1.2",
761 | "on-finished": "2.4.1",
762 | "parseurl": "~1.3.3",
763 | "path-to-regexp": "0.1.7",
764 | "proxy-addr": "~2.0.7",
765 | "qs": "6.11.0",
766 | "range-parser": "~1.2.1",
767 | "safe-buffer": "5.2.1",
768 | "send": "0.18.0",
769 | "serve-static": "1.15.0",
770 | "setprototypeof": "1.2.0",
771 | "statuses": "2.0.1",
772 | "type-is": "~1.6.18",
773 | "utils-merge": "1.0.1",
774 | "vary": "~1.1.2"
775 | }
776 | },
777 | "express-list-endpoints": {
778 | "version": "6.0.0",
779 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz",
780 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg=="
781 | },
782 | "finalhandler": {
783 | "version": "1.2.0",
784 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
785 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
786 | "requires": {
787 | "debug": "2.6.9",
788 | "encodeurl": "~1.0.2",
789 | "escape-html": "~1.0.3",
790 | "on-finished": "2.4.1",
791 | "parseurl": "~1.3.3",
792 | "statuses": "2.0.1",
793 | "unpipe": "~1.0.0"
794 | }
795 | },
796 | "forwarded": {
797 | "version": "0.2.0",
798 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
799 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
800 | },
801 | "fresh": {
802 | "version": "0.5.2",
803 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
804 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
805 | },
806 | "function-bind": {
807 | "version": "1.1.1",
808 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
809 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
810 | },
811 | "get-intrinsic": {
812 | "version": "1.1.3",
813 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
814 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
815 | "requires": {
816 | "function-bind": "^1.1.1",
817 | "has": "^1.0.3",
818 | "has-symbols": "^1.0.3"
819 | }
820 | },
821 | "has": {
822 | "version": "1.0.3",
823 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
824 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
825 | "requires": {
826 | "function-bind": "^1.1.1"
827 | }
828 | },
829 | "has-symbols": {
830 | "version": "1.0.3",
831 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
832 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
833 | },
834 | "http-errors": {
835 | "version": "2.0.0",
836 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
837 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
838 | "requires": {
839 | "depd": "2.0.0",
840 | "inherits": "2.0.4",
841 | "setprototypeof": "1.2.0",
842 | "statuses": "2.0.1",
843 | "toidentifier": "1.0.1"
844 | }
845 | },
846 | "iconv-lite": {
847 | "version": "0.4.24",
848 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
849 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
850 | "requires": {
851 | "safer-buffer": ">= 2.1.2 < 3"
852 | }
853 | },
854 | "inherits": {
855 | "version": "2.0.4",
856 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
857 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
858 | },
859 | "ipaddr.js": {
860 | "version": "1.9.1",
861 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
862 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
863 | },
864 | "media-typer": {
865 | "version": "0.3.0",
866 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
867 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
868 | },
869 | "merge-descriptors": {
870 | "version": "1.0.1",
871 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
872 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
873 | },
874 | "methods": {
875 | "version": "1.1.2",
876 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
877 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
878 | },
879 | "mime": {
880 | "version": "1.6.0",
881 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
882 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
883 | },
884 | "mime-db": {
885 | "version": "1.52.0",
886 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
887 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
888 | },
889 | "mime-types": {
890 | "version": "2.1.35",
891 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
892 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
893 | "requires": {
894 | "mime-db": "1.52.0"
895 | }
896 | },
897 | "ms": {
898 | "version": "2.0.0",
899 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
900 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
901 | },
902 | "negotiator": {
903 | "version": "0.6.3",
904 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
905 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
906 | },
907 | "object-inspect": {
908 | "version": "1.12.2",
909 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
910 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
911 | },
912 | "on-finished": {
913 | "version": "2.4.1",
914 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
915 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
916 | "requires": {
917 | "ee-first": "1.1.1"
918 | }
919 | },
920 | "on-headers": {
921 | "version": "1.0.2",
922 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
923 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
924 | },
925 | "parseurl": {
926 | "version": "1.3.3",
927 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
928 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
929 | },
930 | "path-to-regexp": {
931 | "version": "0.1.7",
932 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
933 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
934 | },
935 | "proxy-addr": {
936 | "version": "2.0.7",
937 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
938 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
939 | "requires": {
940 | "forwarded": "0.2.0",
941 | "ipaddr.js": "1.9.1"
942 | }
943 | },
944 | "qs": {
945 | "version": "6.11.0",
946 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
947 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
948 | "requires": {
949 | "side-channel": "^1.0.4"
950 | }
951 | },
952 | "range-parser": {
953 | "version": "1.2.1",
954 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
955 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
956 | },
957 | "raw-body": {
958 | "version": "2.5.1",
959 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
960 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
961 | "requires": {
962 | "bytes": "3.1.2",
963 | "http-errors": "2.0.0",
964 | "iconv-lite": "0.4.24",
965 | "unpipe": "1.0.0"
966 | }
967 | },
968 | "response-time": {
969 | "version": "2.3.2",
970 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz",
971 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==",
972 | "requires": {
973 | "depd": "~1.1.0",
974 | "on-headers": "~1.0.1"
975 | },
976 | "dependencies": {
977 | "depd": {
978 | "version": "1.1.2",
979 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
980 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
981 | }
982 | }
983 | },
984 | "safe-buffer": {
985 | "version": "5.2.1",
986 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
987 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
988 | },
989 | "safer-buffer": {
990 | "version": "2.1.2",
991 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
992 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
993 | },
994 | "send": {
995 | "version": "0.18.0",
996 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
997 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
998 | "requires": {
999 | "debug": "2.6.9",
1000 | "depd": "2.0.0",
1001 | "destroy": "1.2.0",
1002 | "encodeurl": "~1.0.2",
1003 | "escape-html": "~1.0.3",
1004 | "etag": "~1.8.1",
1005 | "fresh": "0.5.2",
1006 | "http-errors": "2.0.0",
1007 | "mime": "1.6.0",
1008 | "ms": "2.1.3",
1009 | "on-finished": "2.4.1",
1010 | "range-parser": "~1.2.1",
1011 | "statuses": "2.0.1"
1012 | },
1013 | "dependencies": {
1014 | "ms": {
1015 | "version": "2.1.3",
1016 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1017 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1018 | }
1019 | }
1020 | },
1021 | "serve-static": {
1022 | "version": "1.15.0",
1023 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1024 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1025 | "requires": {
1026 | "encodeurl": "~1.0.2",
1027 | "escape-html": "~1.0.3",
1028 | "parseurl": "~1.3.3",
1029 | "send": "0.18.0"
1030 | }
1031 | },
1032 | "setprototypeof": {
1033 | "version": "1.2.0",
1034 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1035 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1036 | },
1037 | "side-channel": {
1038 | "version": "1.0.4",
1039 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
1040 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
1041 | "requires": {
1042 | "call-bind": "^1.0.0",
1043 | "get-intrinsic": "^1.0.2",
1044 | "object-inspect": "^1.9.0"
1045 | }
1046 | },
1047 | "statuses": {
1048 | "version": "2.0.1",
1049 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1050 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
1051 | },
1052 | "toidentifier": {
1053 | "version": "1.0.1",
1054 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1055 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
1056 | },
1057 | "type-is": {
1058 | "version": "1.6.18",
1059 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1060 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1061 | "requires": {
1062 | "media-typer": "0.3.0",
1063 | "mime-types": "~2.1.24"
1064 | }
1065 | },
1066 | "unpipe": {
1067 | "version": "1.0.0",
1068 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1069 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
1070 | },
1071 | "utils-merge": {
1072 | "version": "1.0.1",
1073 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1074 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
1075 | },
1076 | "vary": {
1077 | "version": "1.1.2",
1078 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1079 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
1080 | }
1081 | }
1082 | }
1083 |
--------------------------------------------------------------------------------