├── api
├── .nvmrc
├── Dockerfile.test
├── .gitignore
├── .dockerignore
├── models
│ ├── index.js
│ ├── contact.json
│ ├── contact.models.js
│ └── user.model.js
├── Dockerfile
├── utils
│ ├── error-handler.js
│ ├── async-wrapper.js
│ ├── index.js
│ ├── contact-generator.js
│ ├── hateoas.utils.js
│ ├── contacts.utils.js
│ └── format.utils.js
├── services
│ ├── index.js
│ ├── config.service.js
│ ├── auth
│ │ ├── password.service.js
│ │ └── users-auth.service.js
│ ├── cache.service.js
│ └── contacts.service.js
├── routes
│ ├── index.js
│ ├── v1
│ │ ├── index.js
│ │ ├── groupsV1.js
│ │ └── contactsV1.js
│ ├── v2
│ │ ├── index.js
│ │ └── contactsV2.js
│ ├── redirect.routes.js
│ ├── auth
│ │ └── auth.router.js
│ └── docs
│ │ └── v2
│ │ ├── index.js
│ │ ├── contact.schema.yaml
│ │ └── contactsV2.yaml
├── config
│ ├── index.js
│ ├── limiter.config.js
│ ├── cors.config.js
│ ├── db.config.js
│ └── server.config.js
├── middlewares
│ ├── index.js
│ ├── route-protector.middleware.js
│ ├── swagger.middleware.js
│ └── auth.middleware.js
├── controllers
│ ├── index.js
│ ├── groupsV1.js
│ ├── auth
│ │ └── auth.controller.js
│ ├── contactsV2.js
│ └── contactsV1.js
├── .eslintrc.js
├── server.js
├── tests
│ └── contact.test.js
├── package.json
└── ecosystem.config.json
├── client
├── .gitignore
├── public
│ └── main.js
├── views
│ ├── error.ejs
│ ├── contacts
│ │ ├── add-contact.ejs
│ │ └── list.ejs
│ ├── auth
│ │ ├── signup.ejs
│ │ └── signin.ejs
│ ├── common
│ │ └── add-fields.ejs
│ └── index.ejs
├── Dockerfile
├── ecosystem.config.json
├── package.json
├── index.js
└── package-lock.json
├── README.md
├── docker-compose.yml
└── Dockerrun.aws.json
/api/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
--------------------------------------------------------------------------------
/api/Dockerfile.test:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules
3 |
--------------------------------------------------------------------------------
/api/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules
3 |
--------------------------------------------------------------------------------
/client/public/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BROWSER-SIDE JAVASCRIPT
3 | */
--------------------------------------------------------------------------------
/api/models/index.js:
--------------------------------------------------------------------------------
1 | export { Contact } from "./contact.models";
2 | export { User } from "./user.model";
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RESTful-Web-API-Design-with-Node.js-12-contact-api
2 | REST Contact API for Packt course **RESTful Web API Design with Node.js 12** by Florian GOTO
3 |
--------------------------------------------------------------------------------
/client/views/error.ejs:
--------------------------------------------------------------------------------
1 |
2 |
An error occurred
3 |
<%= errorMessage %>
4 |
Go back home
5 |
6 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /app
4 |
5 | COPY ./package*.json ./
6 | RUN npm ci --production
7 |
8 | COPY ./ ./
9 |
10 | CMD [ "npm", "run", "start:prod" ]
--------------------------------------------------------------------------------
/api/utils/error-handler.js:
--------------------------------------------------------------------------------
1 | export const errorHandler = (message, status = 500) => {
2 | const error = new Error(message);
3 | error.statusCode = status;
4 | return error;
5 | };
6 |
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY ./package*.json ./
6 | RUN npm ci --production
7 |
8 | COPY ./ ./
9 |
10 | CMD [ "npm", "run", "start:prod" ]
--------------------------------------------------------------------------------
/api/utils/async-wrapper.js:
--------------------------------------------------------------------------------
1 | // all errors caught by async wrapper and sent to error handler
2 | // no need for try catch
3 | export const AsyncWrapper = func => (req, res, next) =>
4 | func(req, res, next).catch(next);
5 |
--------------------------------------------------------------------------------
/api/services/index.js:
--------------------------------------------------------------------------------
1 | import ConfigService from "./config.service";
2 | import ContactService from "./contacts.service";
3 | import CacheService from "./cache.service";
4 |
5 | export { CacheService, ContactService, ConfigService };
6 |
--------------------------------------------------------------------------------
/api/routes/index.js:
--------------------------------------------------------------------------------
1 | export { routerV1 } from "./v1";
2 | export { routerV2 } from "./v2";
3 | export { redirectRouter } from "./redirect.routes";
4 | export { authRouter } from "./auth/auth.router";
5 | export { routerV2Docs } from "./docs/v2";
6 |
--------------------------------------------------------------------------------
/api/config/index.js:
--------------------------------------------------------------------------------
1 | import ServerConfig from "./server.config";
2 | import DbConfig from "./db.config";
3 | import CorsConfig from "./cors.config";
4 | import RateLimiterConfig from "./limiter.config";
5 |
6 | export { ServerConfig, DbConfig, CorsConfig, RateLimiterConfig };
7 |
--------------------------------------------------------------------------------
/api/middlewares/index.js:
--------------------------------------------------------------------------------
1 | import RouteProtectorMiddleware from "./route-protector.middleware";
2 | import AuthMiddleware from "./auth.middleware";
3 | import SwaggerMiddleware from "./swagger.middleware";
4 |
5 | export { RouteProtectorMiddleware, AuthMiddleware, SwaggerMiddleware };
6 |
--------------------------------------------------------------------------------
/api/controllers/index.js:
--------------------------------------------------------------------------------
1 | import * as contactsV1 from "./contactsV1";
2 | import * as contactsV2 from "./contactsV2";
3 | import * as groupsV1 from "./groupsV1";
4 | import AuthController from "./auth/auth.controller";
5 |
6 | export { contactsV1, groupsV1, contactsV2, AuthController };
7 |
--------------------------------------------------------------------------------
/api/middlewares/route-protector.middleware.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 |
3 | export default class RouteProtectorMiddleware {
4 | /**
5 | * returns passport middleware
6 | */
7 | authenticate() {
8 | return passport.authenticate("jwt", { session: false });
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/api/routes/v1/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import setCpntactsV1 from "./contactsV1";
3 | import setGroupsV1 from "./groupsV1";
4 |
5 | const router = Router();
6 |
7 | setCpntactsV1(router);
8 | setGroupsV1(router);
9 |
10 | export const routerV1 = {
11 | baseUrl: "/api/v1",
12 | router
13 | };
14 |
--------------------------------------------------------------------------------
/api/services/config.service.js:
--------------------------------------------------------------------------------
1 | export default class ConfigService {
2 | static NODE_ENV = process.env.NODE_ENV;
3 | static MONGO_USER = process.env.MONGO_USER;
4 | static MONGO_PASS = process.env.MONGO_PASS;
5 | static MONGO_HOST = process.env.MONGO_HOST;
6 |
7 | static get(name) {
8 | return process.env[name];
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/api/utils/index.js:
--------------------------------------------------------------------------------
1 | export { generateFakeContacts } from "./contact-generator";
2 | export { errorHandler } from "./error-handler";
3 | export { AsyncWrapper } from "./async-wrapper";
4 | export { generateSelf } from "./hateoas.utils";
5 | import * as fmtUtils from "./format.utils";
6 | import ContactUtil from "./contacts.utils";
7 |
8 | export { fmtUtils, ContactUtil };
9 |
--------------------------------------------------------------------------------
/client/ecosystem.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "CLIENT",
5 | "script": "node index.js",
6 | "instances": 1,
7 | "autorestart": true,
8 | "env_development": {
9 | "NODE_ENV": "development",
10 | "API_SERVICE_DNS": "localhost",
11 | "API_SERVICE_PORT": 3000
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/api/routes/v2/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import setContactsV2 from "./contactsV2";
3 | import { RouteProtectorMiddleware } from "../../middlewares";
4 |
5 | const router = Router();
6 |
7 | router.use(new RouteProtectorMiddleware().authenticate());
8 |
9 | setContactsV2(router);
10 |
11 | export const routerV2 = {
12 | baseUrl: "/api/v2",
13 | router
14 | };
15 |
--------------------------------------------------------------------------------
/api/routes/v1/groupsV1.js:
--------------------------------------------------------------------------------
1 | import { groupsV1 as v1 } from "../../controllers";
2 | import { AsyncWrapper } from "../../utils/async-wrapper";
3 |
4 | export default router => {
5 | // GET /api/v1/groups
6 | router.get("/groups", AsyncWrapper(v1.getGroups));
7 |
8 | // GET /api/v1/groups/:contactId
9 | router.get("/groups/:contactId", AsyncWrapper(v1.getGroupsForContact));
10 | };
11 |
--------------------------------------------------------------------------------
/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "extends": "eslint:recommended",
7 | "globals": {
8 | "Atomics": "readonly",
9 | "SharedArrayBuffer": "readonly"
10 | },
11 | "parserOptions": {
12 | "ecmaVersion": 2018,
13 | "sourceType": "module"
14 | },
15 | "rules": {
16 | }
17 | };
--------------------------------------------------------------------------------
/client/views/contacts/add-contact.ejs:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/api/server.js:
--------------------------------------------------------------------------------
1 | import { ServerConfig } from "./config";
2 | import {
3 | redirectRouter,
4 | routerV1,
5 | routerV2,
6 | authRouter,
7 | routerV2Docs
8 | } from "./routes";
9 |
10 | async function main() {
11 | const PORT = process.env.PORT || 3000;
12 | const server = new ServerConfig({
13 | port: PORT,
14 | // middleware: [],
15 | routers: [redirectRouter, routerV1, routerV2, authRouter, routerV2Docs]
16 | });
17 |
18 | server.listen();
19 | }
20 |
21 | main();
22 |
--------------------------------------------------------------------------------
/api/routes/redirect.routes.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 |
3 | const router = Router();
4 |
5 | // 308 Permanent Redirect (method + body not modified)
6 | router.all("/contacts*", (req, res) => {
7 | res.redirect(308, `/api/v1${req.url}`);
8 | });
9 |
10 | // 308 Permanent Redirect (method + body not modified)
11 | router.all("/groups*", (req, res) => {
12 | res.redirect(308, `/api/v1${req.url}`);
13 | });
14 |
15 | export const redirectRouter = {
16 | baseUrl: "/",
17 | router
18 | };
19 |
--------------------------------------------------------------------------------
/api/routes/auth/auth.router.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import { AuthController } from "../../controllers";
3 | import { AsyncWrapper } from "../../utils";
4 |
5 | const router = Router();
6 | const authController = new AuthController();
7 |
8 | // POST /auth/sign-up
9 | router.post(
10 | "/sign-up",
11 | AsyncWrapper(authController.signUp.bind(authController))
12 | );
13 |
14 | // POST /auth/sign-in
15 | router.post(
16 | "/sign-in",
17 | AsyncWrapper(authController.signIn.bind(authController))
18 | );
19 |
20 | export const authRouter = {
21 | baseUrl: "/auth",
22 | router
23 | };
24 |
--------------------------------------------------------------------------------
/api/routes/docs/v2/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import { SwaggerMiddleware } from "../../../middlewares";
3 |
4 | const swaggerMiddleware = new SwaggerMiddleware(
5 | "2.0.0",
6 | [
7 | {
8 | url: "http://localhost:3000/api/v2"
9 | }
10 | ],
11 | ["./config/*.js","./routes/**/*.yaml"]
12 | );
13 |
14 | const router = Router();
15 | router.use("/", swaggerMiddleware.swaggerUiHandlers);
16 |
17 | // GET /api/docs/v2
18 | router.get("/", swaggerMiddleware.swaggerUiMiddleware);
19 |
20 | export const routerV2Docs = {
21 | baseUrl: "/api/docs/v2",
22 | router
23 | };
24 |
--------------------------------------------------------------------------------
/client/views/auth/signup.ejs:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/client/views/contacts/list.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | | First Name |
6 | Last Name |
7 | Phone |
8 | Email |
9 |
10 |
11 | <% for(const ct of contacts){ %>
12 |
13 | | <%= ct.firstName %> |
14 | <%= ct.lastName %> |
15 | <%= ct.primaryContactNumber %> |
16 | <%= ct.primaryEmailAddress %> |
17 |
18 | <% } %>
19 |
20 |
21 |
--------------------------------------------------------------------------------
/api/models/contact.json:
--------------------------------------------------------------------------------
1 | {
2 | "firstName": "Joe",
3 | "lastName": "Black",
4 | "title": "Mr.",
5 | "company": "Dev Inc.",
6 | "jobTitle": "DevOps Engineer",
7 | "address": "45 imaginary street",
8 | "city": "Namur",
9 | "country": "Belgium",
10 | "primaryContactNumber": "+012345678901",
11 | "otherContactNumbers": ["+023456789987", "+098765432101"],
12 | "primaryEmailAddress": "joe.black@mail.com",
13 | "otherEmailAddresses": ["jblack@othermail.com"],
14 | "groups": ["Dev", "Node.js"],
15 | "socialMedia": [
16 | { "name": "Linkedin", "link": "https://www.linkedin.com/in/joeblack/" },
17 | { "name": "Twitter", "link": "https://www.twitter.com/@joeblack/" }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/api/controllers/groupsV1.js:
--------------------------------------------------------------------------------
1 | import { errorHandler } from "../utils";
2 | import { ObjectID } from "bson";
3 | import { Contact } from "../models";
4 |
5 | export const getGroups = async (req, res) => {
6 | const contacts = await Contact.find();
7 |
8 | // use Set data structure to get array of unique values
9 | res.json([...new Set(contacts.flatMap(conntact => conntact.groups))]);
10 | };
11 |
12 | export const getGroupsForContact = async (req, res, next) => {
13 | const contactId = req.params.contactId;
14 | contactId || next(errorHandler("Please enter a contact ID", 422));
15 |
16 | const contact = await Contact.findOne({
17 | _id: new ObjectID(contactId)
18 | });
19 | res.json(contact.groups);
20 | };
21 |
--------------------------------------------------------------------------------
/api/controllers/auth/auth.controller.js:
--------------------------------------------------------------------------------
1 | import UserAuthService from "../../services/auth/users-auth.service";
2 |
3 | export default class AuthController {
4 | constructor() {
5 | this.userAuthService = new UserAuthService();
6 | }
7 |
8 | async signUp(req, res, next) {
9 | const userToken = await this.userAuthService.signUp(req.body);
10 | return res.json({
11 | token: userToken
12 | });
13 | }
14 |
15 | async signIn(req, res, next) {
16 | const { email, password } = req.body;
17 | const accessToken = await this.userAuthService.signIn(email, password);
18 |
19 | res.status(accessToken ? 200 : 401).json({
20 | ...(accessToken
21 | ? { token: accessToken }
22 | : { message: "Authentication failed" })
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/api/config/limiter.config.js:
--------------------------------------------------------------------------------
1 | import RateLimit from "express-rate-limit";
2 | import RedisStore from "rate-limit-redis";
3 |
4 | export default class RateLimiterConfig {
5 | #client;
6 | #redisURI;
7 | #maxRequests;
8 | #windowMs;
9 |
10 | constructor(limiterConfig) {
11 | this.#client = limiterConfig.client;
12 | this.#redisURI = limiterConfig.redisURI;
13 | this.#maxRequests = limiterConfig.maxRequests;
14 | this.#windowMs = limiterConfig.windowMs;
15 | }
16 |
17 | get redisStoreLimiter() {
18 | return new RateLimit({
19 | store: new RedisStore({
20 | ...(this.#client
21 | ? { client: this.#client }
22 | : { redisURI: this.#redisURI })
23 | }),
24 | max: this.#maxRequests,
25 | windowMs: this.#windowMs
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/api/utils/contact-generator.js:
--------------------------------------------------------------------------------
1 | import { company, name, address, phone, internet } from "faker";
2 |
3 | export const generateFakeContacts = (n = 3) =>
4 | new Array(n).fill('toto').map(() => ({
5 | firstName: name.firstName(),
6 | lastName: name.lastName(),
7 | company: company.companyName(),
8 | jobTitle: name.jobTitle(),
9 | address: address.streetAddress(),
10 | city: address.city(),
11 | country: address.country(),
12 | primaryContactNumber: phone.phoneNumber(),
13 | otherContactNumbers: [phone.phoneNumber(), phone.phoneNumber()],
14 | primaryEmailAddress: internet.email(),
15 | otherEmailAddresses: [internet.email(), internet.email()],
16 | groups: ["Dev", "Node.js", "REST"],
17 | socialMedia: [
18 | { name: "Linkedin", link: internet.url() },
19 | { name: "Twitter", link: internet.url() }
20 | ]
21 | }));
22 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "preinstall": "npm i -g pm2",
8 | "start": "node index.js",
9 | "start:prod": "pm2-runtime ecosystem.config.json",
10 | "start:dev": "pm2-runtime ecosystem.config.json --env development"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "axios": "^0.19.2",
17 | "cors": "^2.8.5",
18 | "ejs": "^3.0.1",
19 | "express": "^4.17.1",
20 | "helmet": "^3.21.2",
21 | "morgan": "^1.9.1",
22 | "pm2": "^4.2.3"
23 | },
24 | "devDependencies": {
25 | "@types/axios": "^0.14.0",
26 | "@types/cors": "^2.8.6",
27 | "@types/ejs": "^3.0.0",
28 | "@types/express": "^4.17.2",
29 | "@types/helmet": "0.0.45",
30 | "@types/morgan": "^1.7.37"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/client/views/auth/signin.ejs:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/api/routes/v1/contactsV1.js:
--------------------------------------------------------------------------------
1 | // use aliases to avoid name conflicts
2 | import { contactsV1 as v1 } from "../../controllers";
3 | import { AsyncWrapper } from "../../utils/async-wrapper";
4 |
5 | export default router => {
6 | // GET /api/v1/contacts
7 | router.get("/contacts", AsyncWrapper(v1.getContacts));
8 |
9 | // GET /api/v1/contacts/:id
10 | router.get("/contacts/:id", AsyncWrapper(v1.getContact));
11 |
12 | // POST /api/v1/contacts
13 | router.post("/contacts", AsyncWrapper(v1.postContact));
14 |
15 | // POST /api/v1/contacts/many?n=X
16 | router.post("/contacts/many", AsyncWrapper(v1.postManyContacts));
17 |
18 | // PUT /api/v1/contacts/:id
19 | router.put("/contacts/:id", AsyncWrapper(v1.putContact));
20 |
21 | // DELETE /api/v1/contacts/:id
22 | router.delete("/contacts/:id", AsyncWrapper(v1.deleteContact));
23 |
24 | // DELETE /api/v1/contacts
25 | router.delete("/contacts", AsyncWrapper(v1.deleteAllContact));
26 | };
27 |
--------------------------------------------------------------------------------
/client/views/common/add-fields.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
41 |
42 |
--------------------------------------------------------------------------------
/api/models/contact.models.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongoosePaginate from "mongoose-paginate-v2";
3 |
4 | //define Model for metadata collection.
5 | export const GFS = mongoose.model(
6 | "GFS",
7 | new mongoose.Schema({}, { strict: false }),
8 | "images.files"
9 | );
10 |
11 | const contactSchema = new mongoose.Schema(
12 | {
13 | firstName: String,
14 | lastName: String,
15 | title: String,
16 | company: String,
17 | jobTitle: String,
18 | address: String,
19 | city: String,
20 | country: String,
21 | primaryContactNumber: String,
22 | otherContactNumbers: [String],
23 | primaryEmailAddress: String,
24 | otherEmailAddresses: [String],
25 | groups: [String],
26 | socialMedia: [
27 | {
28 | name: String,
29 | link: String
30 | }
31 | ],
32 | image: {
33 | type: mongoose.Schema.Types.ObjectId,
34 | ref: "GFS"
35 | }
36 | },
37 | { versionKey: false }
38 | );
39 |
40 | contactSchema.plugin(mongoosePaginate);
41 |
42 | export const Contact = mongoose.model("Contact", contactSchema);
43 |
--------------------------------------------------------------------------------
/api/services/auth/password.service.js:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt";
2 |
3 | export default class PasswordService {
4 | #rounds = 8;
5 |
6 | /**
7 | *
8 | * @param {number} rounds series of rounds to give you a secure hash.
9 | * Bcrypt will use the value you enter and go through 2^rounds iterations of processing
10 | * On a 2GHz core:
11 | * rounds=10: ~10 hashes/sec
12 | * rounds=15: ~3 sec/hash
13 | * rounds=25: ~1 hour/hash
14 | */
15 | constructor(rounds) {
16 | this.#rounds = rounds;
17 | }
18 |
19 | /**
20 | * generate hash from plaintext password
21 | * @param {string} password
22 | */
23 | async hash(password) {
24 | if (!password) throw new Error("Please provide valid password");
25 |
26 | return await bcrypt.hash(password, this.#rounds);
27 | }
28 |
29 | /**
30 | * check password generated from bcrypt
31 | * @param {string} password
32 | * @param {string} hash
33 | */
34 | async check(password, hash) {
35 | if (!password) throw new Error("Please provide valid password");
36 |
37 | return await bcrypt.compare(password, hash);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Contacts App
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 | <% if(error) {%>
21 | <%- include('error', { errorMessage: errorMessage }); %>
22 | <% } %>
23 |
24 | <% if(!error && !auth) {%>
25 | <%- include('auth/signup', {}); %>
26 | <%- include('auth/signin', {}); %>
27 | <% } else if(auth) { %>
28 | CONTACTS:
29 | <%- include('contacts/list', {}); %>
30 | <%- include('contacts/add-contact', {token: token}); %>
31 | <% } %>
32 |
33 |
34 |
--------------------------------------------------------------------------------
/api/middlewares/swagger.middleware.js:
--------------------------------------------------------------------------------
1 | import swaggerJsdoc from "swagger-jsdoc";
2 | import swaggerUi from "swagger-ui-express";
3 |
4 | export default class SwaggerMiddleware {
5 | #specs;
6 |
7 | constructor(version, servers, apis) {
8 | // Swagger set up
9 | const options = {
10 | swaggerDefinition: {
11 | openapi: "3.0.0",
12 | info: {
13 | title: "Contacts API",
14 | version,
15 | description:
16 | "The Contacts API is a prototype API for applying REST architecture best practices",
17 | license: {
18 | name: "MIT",
19 | url: "https://choosealicense.com/licenses/mit/"
20 | },
21 | contact: {
22 | name: "Swagger",
23 | url: "https://swagger.io",
24 | email: "Info@SmartBear.com"
25 | }
26 | },
27 | servers
28 | },
29 | apis
30 | };
31 |
32 | this.#specs = swaggerJsdoc(options);
33 | }
34 |
35 | get swaggerUiHandlers() {
36 | return swaggerUi.serve;
37 | }
38 |
39 | get swaggerUiMiddleware() {
40 | return swaggerUi.setup(this.#specs, {
41 | explorer: true
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/api/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongoosePaginate from "mongoose-paginate-v2";
3 | import { GFS } from "./contact.models";
4 |
5 | const userSchema = new mongoose.Schema(
6 | {
7 | firstName: String,
8 | lastName: String,
9 | title: String,
10 | company: String,
11 | jobTitle: String,
12 | address: String,
13 | city: String,
14 | country: String,
15 | primaryContactNumber: String,
16 | otherContactNumbers: [String],
17 | primaryEmailAddress: {
18 | type: String,
19 | unique: true
20 | },
21 | otherEmailAddresses: [String],
22 | groups: [String],
23 | socialMedia: [
24 | {
25 | name: String,
26 | link: String
27 | }
28 | ],
29 | image: {
30 | type: mongoose.Schema.Types.ObjectId,
31 | ref: "GFS"
32 | },
33 | credential: {
34 | password: String
35 | },
36 | contacts: [
37 | {
38 | type: mongoose.Schema.Types.ObjectId,
39 | ref: "Contact"
40 | }
41 | ]
42 | },
43 | { versionKey: false }
44 | );
45 |
46 | userSchema.plugin(mongoosePaginate);
47 |
48 | export const User = mongoose.model("User", userSchema);
49 |
--------------------------------------------------------------------------------
/api/utils/hateoas.utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate links related to entity
3 | * @param {*} entity
4 | * @param {string} url
5 | */
6 | export const generateSelf = ({ url, entity }) => {
7 | const self = [
8 | {
9 | href: `${url}/api/v2/contacts${entity ? `/${entity._id}` : "{?offset,limit}"}`,
10 | method: "GET",
11 | rel: "self"
12 | }
13 | ];
14 |
15 | if (entity) {
16 | const selfRef = [
17 | ...self,
18 | {
19 | href: `${url}/contacts/${entity._id}`,
20 | method: "PUT",
21 | rel: "update"
22 | },
23 | {
24 | href: `${url}/contacts/${entity._id}`,
25 | method: "DELETE",
26 | rel: "delete"
27 | },
28 | {
29 | href: `${url}/api/v2/contacts/${entity._id}/image`,
30 | method: "POST",
31 | rel: "image"
32 | }
33 | ];
34 |
35 | const imageRef = [
36 | {
37 | href: `${url}/api/v2/contacts/${entity._id}/image`,
38 | method: "GET",
39 | rel: "image"
40 | },
41 |
42 | {
43 | href: `${url}/api/v2/contacts/${entity._id}/image`,
44 | method: "DELETE",
45 | rel: "image"
46 | }
47 | ];
48 | return entity.image ? [...selfRef, ...imageRef] : selfRef;
49 | }
50 | return self;
51 | };
52 |
--------------------------------------------------------------------------------
/api/middlewares/auth.middleware.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import { ConfigService } from "../services";
3 | import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
4 | import { User } from "../models";
5 |
6 | export default class AuthMiddleware {
7 | #configService;
8 |
9 | constructor() {
10 | this.#configService = ConfigService;
11 | }
12 |
13 | /**
14 | * Passport JWT strategy implementation to extract JWT token data and verify
15 | */
16 | registerJwtStrategy() {
17 | const authStrategy = new JwtStrategy(
18 | {
19 | secretOrKey: this.#configService.get("JWT_TOKEN_SECRET"),
20 | algorithms: ["HS256"],
21 | issuer: this.#configService.get("JWT_TOKEN_ISSUER"),
22 | ignoreExpiration: false,
23 | jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme("Bearer")
24 | },
25 | async (jwtPayload, done) => {
26 | const _id = jwtPayload.sub.toString();
27 | if (!_id) return done(null, false);
28 |
29 | try {
30 | let user = await User.findOne({ _id }).select({ credential: 0 });
31 |
32 | // set user on request object
33 | user ? done(null, user) : done(null, false);
34 | } catch (err) {
35 | done(err);
36 | }
37 | }
38 | );
39 |
40 | passport.use(authStrategy);
41 | return passport.initialize();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/api/routes/v2/contactsV2.js:
--------------------------------------------------------------------------------
1 | // use aliases to avoid name conflicts
2 | import { contactsV2 as v2 } from "../../controllers";
3 | import { AsyncWrapper } from "../../utils/async-wrapper";
4 | import { CorsConfig } from "../../config";
5 | import { ConfigService } from "../../services";
6 |
7 | export default async router => {
8 | const corsConf = new CorsConfig(ConfigService.get("CORS_WHITELIST"));
9 |
10 | // GET /api/v2/contacts
11 | router.get(
12 | "/contacts",
13 | (res, req, next) => corsConf.setAsyncConfig()(res, req, next),
14 | AsyncWrapper(v2.getBasicContacts)
15 | );
16 |
17 | // GET /api/v2/contacts/full
18 | router.get("/contacts/full", AsyncWrapper(v2.getContacts));
19 |
20 | // POST /api/v2/contacts/:id/image
21 | router.post("/contacts/:id/image", v2.postContactImage.map(AsyncWrapper));
22 |
23 | // GET /api/v2/contacts/:id/image
24 | router.get("/contacts/:id/image", AsyncWrapper(v2.getContactImage));
25 |
26 | // DELETE /api/v2/contacts/:id/image
27 | router.delete("/contacts/:id/image", AsyncWrapper(v2.deleteContactImage));
28 |
29 | // POST /api/v2/contacts
30 | router.post("/contacts", AsyncWrapper(v2.postUserContact));
31 |
32 | // PUT /api/v2/contacts/:id
33 | router.put("/contacts/:id", AsyncWrapper(v2.putUserContact));
34 |
35 | // DELETE /api/v2/contacts/:id
36 | router.delete("/contacts/:id", AsyncWrapper(v2.deleteUserContact));
37 | };
38 |
--------------------------------------------------------------------------------
/api/config/cors.config.js:
--------------------------------------------------------------------------------
1 | import cors from "cors";
2 |
3 | export default class CorsConfig {
4 | #rawWhitelist;
5 | #whitelist;
6 | #cors;
7 |
8 | /**
9 | *
10 | * @param {string} whitelist double semicolon separated list of URLs
11 | */
12 | constructor(whitelist) {
13 | this.#rawWhitelist = whitelist;
14 | this.#cors = cors;
15 | }
16 |
17 | setAsyncConfig() {
18 | if (this.#rawWhitelist) {
19 | this.#whitelist = this.#rawWhitelist.split(";;");
20 |
21 | const corsOptionsDelegate = (req, callback) => {
22 | const requestOrigin = req.header("Origin");
23 | const isIncluded = this.#whitelist.includes(requestOrigin);
24 | let corsOptions;
25 |
26 | if (isIncluded) {
27 | // reflect (enable) the requested origin in the CORS response
28 | corsOptions = { origin: true };
29 | // callback expects two parameters: error and options
30 | callback(null, corsOptions);
31 | } else {
32 | // disable CORS for this request
33 | corsOptions = { origin: false };
34 | // callback expects two parameters: error and options
35 | callback(new Error("Origin not allowed"), corsOptions);
36 | }
37 | };
38 |
39 | // returns a middleware function
40 | return this.#cors(corsOptionsDelegate);
41 | } else {
42 | // returns a middleware function
43 | return this.#cors();
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/api/routes/docs/v2/contact.schema.yaml:
--------------------------------------------------------------------------------
1 | components:
2 | schemas:
3 | Contact:
4 | type: object
5 | required:
6 | - firstName
7 | - lastName
8 | - primaryContactNumber
9 | - primaryEmailAddress
10 | properties:
11 | firstName:
12 | type: string
13 | description: the contact's firstname
14 | example: Brof
15 | lastName:
16 | type: string
17 | description: the contact's lastName
18 | example: Plack
19 | title:
20 | type: string
21 | description: the contact's title
22 | example: First class admiral
23 | company:
24 | type: string
25 | description: the contact's company
26 | example: Bone brigade
27 | jobTitle:
28 | type: string
29 | description: the contact's job title
30 | example: owner
31 | address:
32 | type: string
33 | city:
34 | type: string
35 | country:
36 | type: string
37 | primaryContactNumber:
38 | type: string
39 | otherContactNumbers:
40 | type: array
41 | items:
42 | type: string
43 | primaryEmailAddress:
44 | type: string
45 | otherEmailAddresses:
46 | type: array
47 | items:
48 | type: string
49 | groups:
50 | type: array
51 | items:
52 | type: string
53 | socialMedia:
54 | type: array
55 | items:
56 | type: object
57 | properties:
58 | name:
59 | type: string
60 | link:
61 | type: string
62 | image:
63 | type: string
64 | description: reference to contact's image file
65 |
--------------------------------------------------------------------------------
/api/tests/contact.test.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import assert from "assert";
3 | import { connectDb } from "../config";
4 | import { Contact } from "../models";
5 |
6 | connectDb("contacts-test");
7 |
8 | // don't change database state
9 |
10 | // Hook running before each test.
11 | beforeEach(done => {
12 | mongoose.connection.collections.contacts
13 | .drop()
14 | .then(() => {
15 | done();
16 | })
17 | .catch(err => {
18 | console.error(err.message);
19 | done();
20 | });
21 | });
22 |
23 | // Hook running after each test.
24 | afterEach(done => {
25 | mongoose.disconnect();
26 | return done();
27 | });
28 |
29 | describe("Create Contact", () => {
30 | it("should insert one new contact", done => {
31 | const createdContact = new Contact({
32 | firstName: "John",
33 | lastName: "TOTO",
34 | otherContactNumbers: ["675-422-2796 x13687", "001-388-8810 x253"],
35 | otherEmailAddresses: ["Rosalyn84@hotmail.com", "Ben71@gmail.com"],
36 | groups: ["Dev", "Node.js"],
37 | company: "Prosacco, Dickens and Gerlach",
38 | jobTitle: "International Configuration Representative",
39 | address: "78334 Dorcas Parkways",
40 | city: "Sagemouth",
41 | country: "Totoland",
42 | primaryContactNumber: "641-611-4904 x03036",
43 | primaryEmailAddress: "Nicolette.Jacobi@yahoo.com",
44 | socialMedia: [
45 | {
46 | _id: "5de6ec65740ebf7f7216190a",
47 | name: "Linkedin",
48 | link: "https://marley.com"
49 | },
50 | {
51 | _id: "5de6ec65740ebf7f72161909",
52 | name: "Twitter",
53 | link: "http://simeon.biz"
54 | }
55 | ]
56 | });
57 |
58 | createdContact
59 | .save()
60 | .then(() => {
61 | assert(!createdContact.isNew);
62 | return done();
63 | })
64 | .catch(error => {
65 | console.error(error.message);
66 | return done();
67 | });
68 | }).timeout(5000);
69 | });
70 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "contact_api",
3 | "version": "1.0.0",
4 | "description": "a contact REST API",
5 | "main": "server.js",
6 | "engines": {
7 | "node": "^12.14.1"
8 | },
9 | "scripts": {
10 | "start": "node -r esm .",
11 | "preinstall": "npm i -g pm2",
12 | "start:prod": "pm2-runtime ecosystem.config.json",
13 | "start:dev": "pm2-runtime ecosystem.config.json --env development",
14 | "debug": "DEBUG=* node -r esm .",
15 | "test": "mocha ./tests/*",
16 | "docker:build": "sudo docker build . -t"
17 | },
18 | "author": "Florian GOTO",
19 | "license": "ISC",
20 | "dependencies": {
21 | "bcrypt": "^3.0.7",
22 | "cors": "^2.8.5",
23 | "esm": "^3.2.25",
24 | "express": "^4.17.1",
25 | "express-basic-auth": "^1.2.0",
26 | "express-paginate": "^1.0.0",
27 | "express-rate-limit": "^5.0.0",
28 | "helmet": "^3.21.2",
29 | "jsonwebtoken": "^8.5.1",
30 | "mongoose": "^5.7.13",
31 | "mongoose-paginate-v2": "^1.3.6",
32 | "morgan": "^1.9.1",
33 | "multer": "^1.4.2",
34 | "multer-gridfs-storage": "^4.0.1",
35 | "passport": "^0.4.1",
36 | "passport-jwt": "^4.0.0",
37 | "pm2": "^4.2.1",
38 | "rate-limit-redis": "^1.7.0",
39 | "faker": "^4.1.0",
40 | "redis": "^2.8.0",
41 | "swagger-jsdoc": "^3.5.0",
42 | "swagger-ui-express": "^4.1.3"
43 | },
44 | "directories": {
45 | "test": "test"
46 | },
47 | "devDependencies": {
48 | "@types/axios": "^0.14.0",
49 | "@types/bcrypt": "^3.0.0",
50 | "@types/cors": "^2.8.6",
51 | "@types/express": "^4.17.2",
52 | "@types/express-paginate": "^1.0.0",
53 | "@types/express-rate-limit": "^3.3.3",
54 | "@types/faker": "^4.1.7",
55 | "@types/helmet": "0.0.45",
56 | "@types/jsonwebtoken": "^8.3.5",
57 | "@types/mongoose": "^5.5.32",
58 | "@types/mongoose-paginate-v2": "^1.3.0",
59 | "@types/morgan": "^1.7.37",
60 | "@types/multer": "^1.3.10",
61 | "@types/node": "^12.12.14",
62 | "@types/passport": "^1.0.2",
63 | "@types/passport-jwt": "^3.0.3",
64 | "@types/rate-limit-redis": "^1.6.0",
65 | "@types/redis": "^2.8.14",
66 | "@types/swagger-jsdoc": "^3.0.2",
67 | "@types/swagger-ui-express": "^4.1.1",
68 | "eslint": "^6.7.0",
69 | "mocha": "^6.2.2",
70 | "nodemon": "^2.0.1",
71 | "supertest": "^4.0.2"
72 | },
73 | "mocha": {
74 | "require": [
75 | "esm"
76 | ]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/api/ecosystem.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "API",
5 | "script": "node -r esm ./",
6 | "instances": 1,
7 | "autorestart": true,
8 | "env_development": {
9 | "NODE_ENV": "development",
10 | "MONGO_USER": "admin",
11 | "MONGO_PASS": "87654321Mongodb",
12 | "MONGO_HOST": "cluster0-y8588.mongodb.net",
13 | "REDIS_PASSWORD": "cmVkaXNwYXNzMQo=",
14 | "REDIS_HOST": "localhost",
15 | "REDIS_PORT": 6379,
16 | "CORS_WHITELIST": "",
17 | "RATE_LIMIT_MAX_REQUESTS": 100,
18 | "RATE_LIMIT_WINDOW_MS": 900000,
19 | "JWT_TOKEN_SECRET": "smcP0mbNPwAIpwpMxajKNtf8EGd1P7I/5ULokS94nv9uUPBkKkNcVoGB1WaFNstd qxPfBQnr9chrR+ObeCWgj+yIjEJ8gQdnQS7I3U7yiOsO4sjKkvXL/J1X4p8TWwg6 GQfJQscXIE30W3T7CoubwqtEGBVWvmY5+IOkRpcYQ6SzcG16JIsmEjDFA01gWAV1 UFZhrGBE1Dm3vFMJeJRz1Nz3ce3NitpMdH0avaldyGL+ndupcp4j3YVLpeyhZkkd EkDJOarOWGdlBDN0Auuom9u2d64MPvdx12uuhJVpkGtXkKhHC1MZcKOCIxwZb66g mGWh3N0nR1RJ8pHJP88nu91+icaZ43K0aj8NXBzRolejsfD24/bxuBrtWselazRI dMnn2KSlu3yfNi4q3jZ6WNKbAAJrxF6cu3YPOvBkSDnGuBsL+S37c0E2KC5EDIzX 76wcpdw58SHyCDXQptKXejhq9NuAhJNkAmdFiz/pPB/QJwDCBIT+pjLwLomPsKM0 wMDMMDb86kXhaisjn7xgC0hqd32MrP0SwuLG2IcqggHl+m7xBs3+OwbWnmGE2EFH 6xNO+YY/T6m8R+1Dx9s03EpZOIMU+rkIFWgOupZL3M9RwRDTSKIJqo6VOvQgpjjb JnPzWYxKgtD9CIttRnodBLBOiK5ERLOhEvpZYDzMJK783np+4a9nE0LkvRcNcZNH 1rv6EJZvpQxhpEm4I+HJ5p9hy7YkmBpaZ4GKtZojicMwtL2vRuFdH+WYgGYdPVxD r3PKQRk+ktPblnJFE3OQxMfBpgF44Qn4p65D68gzclcfuNaRTb8GmCIBaRj1N/wd e5FbJJVcoEstwOyALWug7ZMjo8/S3hF9fkXAqp53P2nLL0PsQ9pLptC7fLhQ83TR aPvEaZ8pbpStzLOSVMwXQMkZwK4/2QysV26NWAO5nU+KJifFH9gDGPKTzk7i462P pnWQCuuV+UGLv610woRdXpJm7A7lnl/8M+3nC4Uj28QpFnKiTxM3G9NILkmhMzai PobhcTCqvna+qgvcTuWkQSHnQteKWca8n713Q06UuKD4iWqO18OH/OuTsB7++AG2 zM3jzK0ZmT5gcglRD5dD/Daz3wPuo/7foK9fBDsFZ9Uqq4yw9bKcRmBe3hPAiwRe ZJtldTa3y/4okfdJvHbD+tcPoImElLd46EgPV3w8i6aiDp3cxjr3k76nBjPC+hso P9WEIeIQhsI5n6G49EKLEZV5X5dyO4Fp8vHwON5GufMzGWFzxUkPSj53FpL4SdNc gypan9EiVoeJ+BkObTXwJNrgcMDmcvU+v5nYiusXAU3odv2HbPvewmFEpCcTfqHp a6EjdIMuN4YTD7UsAXNbXXHp7AumwLJVEBINT2kC0r0hGQGmcuL3bj66voFk+JHX UcLgD7PWhBGvVRhwbZpXgQ9/KXcft+PySicZfw0hwr0SR5/Zgy6jI0Z8lL8vXl5z psBdefl9fTQafqD8PXHhN4GTx0TF7emC9rzwuGF1t6Z3KZOSVpkPArK3+hPSmJAt /fQYIdcPVpKQU2GJcSO8aVeRz4uRisLV5aqU+7XegR6ASXcn2pE7lV9A96wg3JMm K5mYfT3gAejnH+7BeRY7OoaZ/o7xF9xcStZ90feJYlqRFnkOJyIO+6wyrLHPhMru k2X4uhzSB2zByN8q6UI176W+YbgNt4nlPU8AoYgE2ElOo+B6GcPyE4l8vnAem9I4 SyC4AIhqpkzyhUs4b3XngfBF6+Mgk23wJjrahSQ2dxPvpARw+KIhxEywema8qeB+ lLVHe/qW9JYjJ4jUkjoEHk47zai7Mo8BctWtZ4h+MQuRKKYVz/9bWq0p0oPXQQ/i 702qw+frFNE=",
20 | "JWT_TOKEN_ISSUER": "contactsdb.com"
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/api/config/db.config.js:
--------------------------------------------------------------------------------
1 | import multer from "multer";
2 | import mongoose from "mongoose";
3 | import crypto from "crypto";
4 | import path from "path";
5 | import GridFsStorage from "multer-gridfs-storage";
6 |
7 | import { ConfigService } from "../services";
8 | // eslint-disable-next-line no-unused-vars
9 | import { GridFSBucket } from "mongodb";
10 |
11 | export default class DbConfig {
12 | /** @type {mongoose.Connection} */
13 | static conn;
14 | /** @type {string} */
15 | static mongoURI;
16 | /** @type {multer.Instance} */
17 | static upload;
18 | /** @type {GridFSBucket} */
19 | static gfsBucket;
20 |
21 | constructor(dbName, bucketName) {
22 | this.dbName = dbName;
23 | this.bucketName = bucketName;
24 | this.mongoURI = `mongodb+srv://${ConfigService.MONGO_USER}:${ConfigService.MONGO_PASS}@${ConfigService.MONGO_HOST}/${dbName}?retryWrites=true&w=majority`;
25 | }
26 |
27 | async connectDb() {
28 | mongoose.connect(this.mongoURI, {
29 | useUnifiedTopology: true,
30 | useCreateIndex: true
31 | });
32 |
33 | // retrieve mongoose default connection
34 | DbConfig.conn = mongoose.connection;
35 |
36 | DbConfig.conn.once("open", () => {
37 | console.log(`Connected to ${this.dbName} database`);
38 |
39 | DbConfig.gfsBucket = new mongoose.mongo.GridFSBucket(DbConfig.conn.db, {
40 | bucketName: this.bucketName
41 | });
42 | });
43 |
44 | DbConfig.conn.on("error", err => console.error(err.message));
45 |
46 | const storage = new GridFsStorage({
47 | db: DbConfig.conn,
48 | file: (req, file) => {
49 | return new Promise((resolve, reject) => {
50 | crypto.randomBytes(16, (err, buf) => {
51 | if (err) {
52 | return reject(err);
53 | }
54 | const filename =
55 | buf.toString("hex") + path.extname(file.originalname);
56 | const fileInfo = {
57 | metadata: {
58 | originalName: file.originalname
59 | },
60 | filename: filename,
61 | bucketName: this.bucketName
62 | };
63 | resolve(fileInfo);
64 | });
65 | });
66 | }
67 | });
68 |
69 | DbConfig.upload = multer({
70 | storage,
71 | fileFilter: (req, file, cb) => {
72 | file.mimetype.includes("image")
73 | ? cb(null, true)
74 | : cb(new Error("Wrong file type - only accepts images"));
75 | }
76 | });
77 |
78 | return DbConfig.conn;
79 | }
80 |
81 | static getMulterUploadMiddleware() {
82 | return async (...args) => DbConfig.upload.single("file")(...args);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/api/services/auth/users-auth.service.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import PasswordService from "./password.service";
3 | import ConfigService from "../config.service";
4 | import { User } from "../../models";
5 |
6 | export default class UserAuthService {
7 | #passwordService;
8 | #configService;
9 |
10 | constructor() {
11 | this.#passwordService = new PasswordService(10);
12 | this.#configService = ConfigService;
13 | }
14 |
15 | /**
16 | *
17 | * @param {*} user
18 | */
19 | async signUp(user) {
20 | if (!user) throw new Error("Please provide valid user");
21 |
22 | const usr = user;
23 | const hashedPassword = await this.#passwordService.hash(
24 | usr.credential.password
25 | );
26 |
27 | usr.credential.password = hashedPassword;
28 |
29 | const insertedUser = await new User(usr).save();
30 | const userPOJO = insertedUser.toObject();
31 |
32 | // console.log("==== USER ===== %o", userPOJO);
33 |
34 | return this.generateAccessToken(userPOJO);
35 | }
36 |
37 | /**
38 | *
39 | * @param {string} email
40 | * @param {string} password
41 | */
42 | async signIn(email, password) {
43 | const user = await User.findOne({ primaryEmailAddress: email });
44 |
45 | if (!user) return null;
46 |
47 | if (
48 | (await this.#passwordService.check(
49 | password,
50 | user.credential.password
51 | )) === true
52 | ) {
53 | return this.generateAccessToken(user.toObject());
54 | }
55 |
56 | return null;
57 | }
58 |
59 | /**
60 | *
61 | * @param {*} user
62 | */
63 | generateAccessToken(user) {
64 | "use strict";
65 |
66 | if (!user) throw new Error("Please enter valid user");
67 |
68 | let userData = null;
69 |
70 | // use block of code to forbid from upper-level method code
71 | {
72 | // nullify credential
73 | userData = { ...user, credential: null };
74 |
75 | // remove credential property
76 | delete userData.credential;
77 |
78 | // make immutable
79 | Object.freeze(userData);
80 | }
81 |
82 | const jwtPayload = {
83 | user: userData
84 | };
85 |
86 | console.log(
87 | "ISSUER",
88 | this.#configService.get("JWT_TOKEN_ISSUER"),
89 | typeof this.#configService.get("JWT_TOKEN_ISSUER")
90 | );
91 |
92 | const token = jwt.sign(
93 | jwtPayload,
94 | this.#configService.get("JWT_TOKEN_SECRET"),
95 | {
96 | algorithm: "HS256",
97 | issuer: this.#configService.get("JWT_TOKEN_ISSUER"),
98 | subject: userData._id.toString()
99 | }
100 | );
101 |
102 | return token;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/api/utils/contacts.utils.js:
--------------------------------------------------------------------------------
1 | export default class ContactUtil {
2 | /**
3 | *
4 | * @param {*} cacheService
5 | * @param {import("mongoose").Model} ContactModel
6 | * @param {import("mongodb").GridFSBucket} gfsBucket
7 | */
8 | constructor(cacheService, ContactModel, gfsBucket) {
9 | this.cacheService = cacheService;
10 | this.ContactModel = ContactModel;
11 | this.gfsBucket = gfsBucket;
12 | }
13 |
14 | /**
15 | *
16 | * @param {import("mongoose").Schema.Types.ObjectId} userId
17 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
18 | */
19 | async getContact(userId, contactId) {
20 | let cachedContact = await this.cacheService.getContact(userId, contactId);
21 |
22 | if (cachedContact) {
23 | console.log(
24 | `========= using contact cache =============== ${JSON.stringify(
25 | cachedContact
26 | )}`
27 | );
28 | }
29 | if (!cachedContact) {
30 | console.log("========= setting contact cache ===============");
31 |
32 | this.cacheService.purgeCacheNow(userId);
33 |
34 | cachedContact = await this.ContactModel.findOne({
35 | _id: contactId
36 | });
37 |
38 | this.cacheService.storeContact(userId, contactId, cachedContact);
39 | }
40 |
41 | return cachedContact;
42 | }
43 |
44 | /**
45 | *
46 | * @param {import("mongoose").Schema.Types.ObjectId} userId
47 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
48 | * @param {{ image: import("mongoose").Schema.Types.ObjectId; }} updateData
49 | */
50 | async updateContact(userId, contactId, updateData) {
51 | await this.cacheService.purgeCacheNow(userId);
52 |
53 | const updatedContact = await this.ContactModel.findOneAndUpdate(
54 | { _id: contactId },
55 | { ...updateData },
56 | { new: true }
57 | );
58 |
59 | console.log(
60 | "==== Updated to: ",
61 | JSON.stringify(updatedContact, null, 2),
62 | "\n\n\n"
63 | );
64 |
65 | this.cacheService.storeContact(userId, contactId, updatedContact);
66 |
67 | return updatedContact;
68 | }
69 |
70 | async removeExistingImage(imageObjectId, userId, contactId) {
71 | await this.cacheService.purgeCacheNow(userId);
72 |
73 | return new Promise((resolve, reject) => {
74 | this.gfsBucket.delete(imageObjectId, async err => {
75 | if (err) return reject(err);
76 |
77 | if (userId && contactId) {
78 | // remove reference to deleted image from contact
79 | const updated = await this.updateContact(userId, contactId, {
80 | image: null
81 | });
82 |
83 | return resolve(updated);
84 | } else {
85 | // orphan image
86 | return Promise.resolve();
87 | }
88 | });
89 | });
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | api:
4 | image: "fgoto/contacts-api"
5 | restart: always
6 | ports:
7 | - "3000:3000"
8 | links:
9 | - redis
10 | environment:
11 | - NODE_ENV=production
12 | - REDIS_HOST=redis
13 | - REDIS_PORT=6379
14 | - REDIS_PASSWORD=cmVkaXNwYXNzMQo=
15 | - MONGO_USER=admin
16 | - MONGO_PASS=87654321Mongodb
17 | - MONGO_HOST=cluster0-y8588.mongodb.net
18 | #- CORS_WHITELIST=
19 | - RATE_LIMIT_MAX_REQUESTS=100
20 | - RATE_LIMIT_WINDOW_MS=900000
21 | - JWT_TOKEN_SECRET=smcP0mbNPwAIpwpMxajKNtf8EGd1P7I/5ULokS94nv9uUPBkKkNcVoGB1WaFNstd qxPfBQnr9chrR+ObeCWgj+yIjEJ8gQdnQS7I3U7yiOsO4sjKkvXL/J1X4p8TWwg6 GQfJQscXIE30W3T7CoubwqtEGBVWvmY5+IOkRpcYQ6SzcG16JIsmEjDFA01gWAV1 UFZhrGBE1Dm3vFMJeJRz1Nz3ce3NitpMdH0avaldyGL+ndupcp4j3YVLpeyhZkkd EkDJOarOWGdlBDN0Auuom9u2d64MPvdx12uuhJVpkGtXkKhHC1MZcKOCIxwZb66g mGWh3N0nR1RJ8pHJP88nu91+icaZ43K0aj8NXBzRolejsfD24/bxuBrtWselazRI dMnn2KSlu3yfNi4q3jZ6WNKbAAJrxF6cu3YPOvBkSDnGuBsL+S37c0E2KC5EDIzX 76wcpdw58SHyCDXQptKXejhq9NuAhJNkAmdFiz/pPB/QJwDCBIT+pjLwLomPsKM0 wMDMMDb86kXhaisjn7xgC0hqd32MrP0SwuLG2IcqggHl+m7xBs3+OwbWnmGE2EFH 6xNO+YY/T6m8R+1Dx9s03EpZOIMU+rkIFWgOupZL3M9RwRDTSKIJqo6VOvQgpjjb JnPzWYxKgtD9CIttRnodBLBOiK5ERLOhEvpZYDzMJK783np+4a9nE0LkvRcNcZNH 1rv6EJZvpQxhpEm4I+HJ5p9hy7YkmBpaZ4GKtZojicMwtL2vRuFdH+WYgGYdPVxD r3PKQRk+ktPblnJFE3OQxMfBpgF44Qn4p65D68gzclcfuNaRTb8GmCIBaRj1N/wd e5FbJJVcoEstwOyALWug7ZMjo8/S3hF9fkXAqp53P2nLL0PsQ9pLptC7fLhQ83TR aPvEaZ8pbpStzLOSVMwXQMkZwK4/2QysV26NWAO5nU+KJifFH9gDGPKTzk7i462P pnWQCuuV+UGLv610woRdXpJm7A7lnl/8M+3nC4Uj28QpFnKiTxM3G9NILkmhMzai PobhcTCqvna+qgvcTuWkQSHnQteKWca8n713Q06UuKD4iWqO18OH/OuTsB7++AG2 zM3jzK0ZmT5gcglRD5dD/Daz3wPuo/7foK9fBDsFZ9Uqq4yw9bKcRmBe3hPAiwRe ZJtldTa3y/4okfdJvHbD+tcPoImElLd46EgPV3w8i6aiDp3cxjr3k76nBjPC+hso P9WEIeIQhsI5n6G49EKLEZV5X5dyO4Fp8vHwON5GufMzGWFzxUkPSj53FpL4SdNc gypan9EiVoeJ+BkObTXwJNrgcMDmcvU+v5nYiusXAU3odv2HbPvewmFEpCcTfqHp a6EjdIMuN4YTD7UsAXNbXXHp7AumwLJVEBINT2kC0r0hGQGmcuL3bj66voFk+JHX UcLgD7PWhBGvVRhwbZpXgQ9/KXcft+PySicZfw0hwr0SR5/Zgy6jI0Z8lL8vXl5z psBdefl9fTQafqD8PXHhN4GTx0TF7emC9rzwuGF1t6Z3KZOSVpkPArK3+hPSmJAt /fQYIdcPVpKQU2GJcSO8aVeRz4uRisLV5aqU+7XegR6ASXcn2pE7lV9A96wg3JMm K5mYfT3gAejnH+7BeRY7OoaZ/o7xF9xcStZ90feJYlqRFnkOJyIO+6wyrLHPhMru k2X4uhzSB2zByN8q6UI176W+YbgNt4nlPU8AoYgE2ElOo+B6GcPyE4l8vnAem9I4 SyC4AIhqpkzyhUs4b3XngfBF6+Mgk23wJjrahSQ2dxPvpARw+KIhxEywema8qeB+ lLVHe/qW9JYjJ4jUkjoEHk47zai7Mo8BctWtZ4h+MQuRKKYVz/9bWq0p0oPXQQ/i 702qw+frFNE=
22 | - JWT_TOKEN_ISSUER=contactsdb.com
23 | client:
24 | image: "fgoto/contacts-client"
25 | restart: always
26 | ports:
27 | - "80:5000"
28 | links:
29 | - api
30 | environment:
31 | - NODE_ENV=production
32 | - API_SERVICE_DNS=api
33 | - API_SERVICE_PORT=3000
34 | redis:
35 | image: redis
36 | environment:
37 | - REDIS_PASSWORD=cmVkaXNwYXNzMQo=
38 |
--------------------------------------------------------------------------------
/api/services/cache.service.js:
--------------------------------------------------------------------------------
1 | import redis from "redis";
2 |
3 | import { promisify } from "util";
4 |
5 | export default class CacheService {
6 | #getContactsForPageField = page => `contacts#${page}`;
7 | #getContactField = contactId => `contact#${contactId}`;
8 | #getUserKey = userId => `user#${userId}`;
9 | #client;
10 | #asyncHget;
11 | #asyncHset;
12 | #asyncExpire;
13 | #asyncTTL;
14 |
15 | constructor(redisConfig) {
16 | this.#client = redis.createClient({
17 | host: redisConfig.host,
18 | port: redisConfig.port,
19 | auth_pass: redisConfig.password
20 | });
21 |
22 | this.#client.on("connect", () => {
23 | console.log("Connected to Redis caching server");
24 | });
25 |
26 | this.#client.on("error", console.error);
27 |
28 | // promisify Redis Node client API
29 | this.#asyncHget = promisify(this.#client.hget).bind(this.#client);
30 | this.#asyncHset = promisify(this.#client.hset).bind(this.#client);
31 | this.#asyncExpire = promisify(this.#client.expire).bind(this.#client);
32 | }
33 |
34 | /**
35 | *
36 | * @param {import("mongoose").Schema.Types.ObjectId} userId
37 | * @param {number} page
38 | */
39 | async getContactsForPage(userId, page = 1) {
40 | const usId = this.#getUserKey(userId);
41 | const ctp = this.#getContactsForPageField(page);
42 | const contacts = await this.#asyncHget(usId, ctp);
43 |
44 | return contacts ? JSON.parse(contacts) : null;
45 | }
46 |
47 | /**
48 | *
49 | * @param {import("mongoose").Schema.Types.ObjectId} userId
50 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
51 | */
52 | async getContact(userId, contactId) {
53 | const usId = this.#getUserKey(userId);
54 | const ctId = this.#getContactField(contactId);
55 | const contact = await this.#asyncHget(usId, ctId);
56 |
57 | return contact ? JSON.parse(contact) : null;
58 | }
59 |
60 | /**
61 | *
62 | * @param {import("mongoose").Schema.Types.ObjectId} userId
63 | * @param {Array} contacts
64 | * @param {number} page
65 | */
66 | async storeContactsForPage(userId, contacts, page = 1) {
67 | const usId = this.#getUserKey(userId);
68 |
69 | const stored = await this.#asyncHset(
70 | usId,
71 | this.#getContactsForPageField(page),
72 | JSON.stringify(contacts)
73 | );
74 | // set auto expire after 1 day
75 | await this.#asyncExpire(usId, 60 * 60 * 24);
76 |
77 | return stored;
78 | }
79 |
80 | /**
81 | *
82 | * @param {import("mongoose").Schema.Types.ObjectId} userId
83 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
84 | * @param {*} data
85 | */
86 | async storeContact(userId, contactId, data) {
87 | const usId = this.#getUserKey(userId);
88 | const ctId = this.#getContactField(contactId);
89 | const stored = await this.#asyncHset(usId, ctId, JSON.stringify(data));
90 |
91 | // set auto expire after 1 minute
92 | await this.#asyncExpire(usId, 60);
93 |
94 | return stored;
95 | }
96 |
97 | /**
98 | *
99 | * @param {import("mongoose").Schema.Types.ObjectId} userId
100 | */
101 | async purgeCacheNow(userId) {
102 | const usId = this.#getUserKey(userId);
103 | return await this.#asyncExpire(usId, 0);
104 | }
105 |
106 | /**
107 | *
108 | * @param {import("mongoose").Schema.Types.ObjectId} userId
109 | * @param {number} seconds
110 | */
111 | async purgeCache(userId, seconds) {
112 | const usId = this.#getUserKey(userId);
113 | return await this.#asyncExpire(usId, seconds);
114 | }
115 |
116 | get redisRateLimitClient() {
117 | return this.#client;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Dockerrun.aws.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSEBDockerrunVersion": 2,
3 | "containerDefinitions": [
4 | {
5 | "name": "api",
6 | "image": "fgoto/contacts-api",
7 | "hostname": "api",
8 | "essential": true,
9 | "memory": 128,
10 | "links": ["redis"],
11 | "environment": [
12 | {
13 | "name": "NODE_ENV",
14 | "value": "production"
15 | },
16 | {
17 | "name": "REDIS_HOST",
18 | "value": "redis"
19 | },
20 | {
21 | "name": "REDIS_PORT",
22 | "value": "6379"
23 | },
24 | {
25 | "name": "REDIS_PASSWORD",
26 | "value": "cmVkaXNwYXNzMQo="
27 | },
28 | {
29 | "name": "MONGO_USER",
30 | "value": "admin"
31 | },
32 | {
33 | "name": "MONGO_PASS",
34 | "value": "87654321Mongodb"
35 | },
36 | {
37 | "name": "MONGO_HOST",
38 | "value": "cluster0-y8588.mongodb.net"
39 | },
40 | {
41 | "name": "RATE_LIMIT_MAX_REQUESTS",
42 | "value": 100
43 | },
44 | {
45 | "name": "RATE_LIMIT_WINDOW_MS",
46 | "value": 900000
47 | },
48 | {
49 | "name": "JWT_TOKEN_SECRET",
50 | "value": "smcP0mbNPwAIpwpMxajKNtf8EGd1P7I/5ULokS94nv9uUPBkKkNcVoGB1WaFNstd qxPfBQnr9chrR+ObeCWgj+yIjEJ8gQdnQS7I3U7yiOsO4sjKkvXL/J1X4p8TWwg6 GQfJQscXIE30W3T7CoubwqtEGBVWvmY5+IOkRpcYQ6SzcG16JIsmEjDFA01gWAV1 UFZhrGBE1Dm3vFMJeJRz1Nz3ce3NitpMdH0avaldyGL+ndupcp4j3YVLpeyhZkkd EkDJOarOWGdlBDN0Auuom9u2d64MPvdx12uuhJVpkGtXkKhHC1MZcKOCIxwZb66g mGWh3N0nR1RJ8pHJP88nu91+icaZ43K0aj8NXBzRolejsfD24/bxuBrtWselazRI dMnn2KSlu3yfNi4q3jZ6WNKbAAJrxF6cu3YPOvBkSDnGuBsL+S37c0E2KC5EDIzX 76wcpdw58SHyCDXQptKXejhq9NuAhJNkAmdFiz/pPB/QJwDCBIT+pjLwLomPsKM0 wMDMMDb86kXhaisjn7xgC0hqd32MrP0SwuLG2IcqggHl+m7xBs3+OwbWnmGE2EFH 6xNO+YY/T6m8R+1Dx9s03EpZOIMU+rkIFWgOupZL3M9RwRDTSKIJqo6VOvQgpjjb JnPzWYxKgtD9CIttRnodBLBOiK5ERLOhEvpZYDzMJK783np+4a9nE0LkvRcNcZNH 1rv6EJZvpQxhpEm4I+HJ5p9hy7YkmBpaZ4GKtZojicMwtL2vRuFdH+WYgGYdPVxD r3PKQRk+ktPblnJFE3OQxMfBpgF44Qn4p65D68gzclcfuNaRTb8GmCIBaRj1N/wd e5FbJJVcoEstwOyALWug7ZMjo8/S3hF9fkXAqp53P2nLL0PsQ9pLptC7fLhQ83TR aPvEaZ8pbpStzLOSVMwXQMkZwK4/2QysV26NWAO5nU+KJifFH9gDGPKTzk7i462P pnWQCuuV+UGLv610woRdXpJm7A7lnl/8M+3nC4Uj28QpFnKiTxM3G9NILkmhMzai PobhcTCqvna+qgvcTuWkQSHnQteKWca8n713Q06UuKD4iWqO18OH/OuTsB7++AG2 zM3jzK0ZmT5gcglRD5dD/Daz3wPuo/7foK9fBDsFZ9Uqq4yw9bKcRmBe3hPAiwRe ZJtldTa3y/4okfdJvHbD+tcPoImElLd46EgPV3w8i6aiDp3cxjr3k76nBjPC+hso P9WEIeIQhsI5n6G49EKLEZV5X5dyO4Fp8vHwON5GufMzGWFzxUkPSj53FpL4SdNc gypan9EiVoeJ+BkObTXwJNrgcMDmcvU+v5nYiusXAU3odv2HbPvewmFEpCcTfqHp a6EjdIMuN4YTD7UsAXNbXXHp7AumwLJVEBINT2kC0r0hGQGmcuL3bj66voFk+JHX UcLgD7PWhBGvVRhwbZpXgQ9/KXcft+PySicZfw0hwr0SR5/Zgy6jI0Z8lL8vXl5z psBdefl9fTQafqD8PXHhN4GTx0TF7emC9rzwuGF1t6Z3KZOSVpkPArK3+hPSmJAt /fQYIdcPVpKQU2GJcSO8aVeRz4uRisLV5aqU+7XegR6ASXcn2pE7lV9A96wg3JMm K5mYfT3gAejnH+7BeRY7OoaZ/o7xF9xcStZ90feJYlqRFnkOJyIO+6wyrLHPhMru k2X4uhzSB2zByN8q6UI176W+YbgNt4nlPU8AoYgE2ElOo+B6GcPyE4l8vnAem9I4 SyC4AIhqpkzyhUs4b3XngfBF6+Mgk23wJjrahSQ2dxPvpARw+KIhxEywema8qeB+ lLVHe/qW9JYjJ4jUkjoEHk47zai7Mo8BctWtZ4h+MQuRKKYVz/9bWq0p0oPXQQ/i 702qw+frFNE="
51 | },
52 | {
53 | "name": "JWT_TOKEN_ISSUER",
54 | "value": "contactsdb.com"
55 | }
56 | ],
57 | "portMappings": [
58 | {
59 | "hostPort": 80,
60 | "containerPort": 3000
61 | }
62 | ]
63 | },
64 | {
65 | "name": "redis",
66 | "image": "redis",
67 | "hostname": "redis",
68 | "essential": true,
69 | "memory": 128,
70 | "environment": [
71 | {
72 | "name": "REDIS_PASSWORD",
73 | "value": "cmVkaXNwYXNzMQo="
74 | }
75 | ],
76 | "portMappings": [
77 | {
78 | "hostPort": 6379,
79 | "containerPort": 6379
80 | }
81 | ]
82 | }
83 | ]
84 | }
85 |
--------------------------------------------------------------------------------
/api/controllers/contactsV2.js:
--------------------------------------------------------------------------------
1 | import { contactsV1 } from ".";
2 | import { ContactService } from "../services";
3 |
4 | import DbConfig from "../config/db.config";
5 |
6 | const contactService = new ContactService();
7 |
8 | export const getBasicContacts = async (req, res, next) => {
9 | const url = `${req.protocol}://${req.hostname}:${req.app.get("port")}`;
10 |
11 | const userId = req.user._id.toString();
12 |
13 | contactService.setAsyncDependencies();
14 |
15 | const filter = req.body.filter;
16 | const offset = +req.query.offset;
17 | const limit = +req.query.limit;
18 | const fields = {
19 | firstName: 1,
20 | lastName: 1,
21 | primaryContactNumber: 1,
22 | primaryEmailAddress: 1,
23 | image: 1
24 | };
25 |
26 | const contacts = await contactService.findContacts(
27 | userId,
28 | filter,
29 | fields,
30 | offset,
31 | limit,
32 | next
33 | );
34 |
35 | contacts &&
36 | res.format({
37 | json() {
38 | res.json({
39 | ...contacts,
40 | docs: contactService.generateLinkedContacts(contacts.docs, url)
41 | });
42 | },
43 | html() {
44 | res.send(contactService.generateHTMLContacts(contacts.docs));
45 | },
46 | csv() {
47 | res.send(contactService.generateCSVContacts(contacts.docs));
48 | },
49 | text() {
50 | res.send(contactService.generateTextContacts(contacts.docs));
51 | }
52 | });
53 | };
54 |
55 | export const getContacts = contactsV1.getContacts;
56 |
57 | export const postContactImage = [
58 | DbConfig.getMulterUploadMiddleware(),
59 | async (req, res, next) => {
60 | const userId = req.user._id.toString();
61 | const contactId = req.params.id;
62 |
63 | contactService.setAsyncDependencies();
64 |
65 | if (req.file) {
66 | const uploaded = await contactService.attachContactImage(
67 | userId,
68 | contactId,
69 | req.file,
70 | next
71 | );
72 |
73 | return uploaded && res.json(req.file);
74 | }
75 |
76 | next(new Error("No uploaded file."));
77 | }
78 | ];
79 |
80 | export const getContactImage = async (req, res, next) => {
81 | const userId = req.user._id.toString();
82 | const contactId = req.params.id;
83 |
84 | contactService.setAsyncDependencies();
85 |
86 | await contactService.getContactImage(userId, contactId, res, next);
87 | };
88 |
89 | export const deleteContactImage = async (req, res, next) => {
90 | const userId = req.user._id.toString();
91 | const contactId = req.params.id;
92 |
93 | contactService.setAsyncDependencies();
94 |
95 | const deleted = await contactService.deleteContactImage(
96 | userId,
97 | contactId,
98 | next
99 | );
100 |
101 | return deleted && res.json({ message: "Image removed" });
102 | };
103 |
104 | export const postUserContact = async (req, res, next) => {
105 | const userId = req.user._id;
106 | const contactData = req.body;
107 |
108 | const created = await contactService.postUserContact(
109 | userId,
110 | contactData,
111 | next
112 | );
113 |
114 | res.status(created ? 201 : 500).json({ created });
115 | };
116 |
117 | export const putUserContact = async (req, res, next) => {
118 | const user = req.user;
119 | const contactId = req.params.id;
120 | const contactData = req.body;
121 |
122 | const updated = await contactService.putUserContact(
123 | user,
124 | contactId,
125 | contactData,
126 | next
127 | );
128 |
129 | res.status(updated ? 200 : 500).json({ updated });
130 | };
131 |
132 | export const deleteUserContact = async (req, res, next) => {
133 | const user = req.user;
134 | const contactId = req.params.id;
135 |
136 | const deleted = await contactService.deleteUserContact(user, contactId, next);
137 |
138 | res.status(deleted ? 200 : 500).json({ deleted });
139 | };
140 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const cors = require("cors");
3 | const helmet = require("helmet");
4 | const morgan = require("morgan");
5 | const axios = require("axios");
6 |
7 | const PORT = process.env.PORT || 5000;
8 | const API_SERVICE_DNS = process.env.API_SERVICE_DNS;
9 | const API_SERVICE_PORT = process.env.API_SERVICE_PORT;
10 | const app = express();
11 |
12 | app.use(cors());
13 | app.use(helmet());
14 | app.use(morgan("short"));
15 | app.use(express.urlencoded()); // put form data on req body
16 | app.use(express.json());
17 |
18 | // set template engine
19 | app.set("view engine", "ejs");
20 | // serve static files
21 | app.use(express.static("public"));
22 |
23 | app.get("/", (req, res) => {
24 | // get authorization header
25 | const authHeader = req.get("Authorization");
26 |
27 | if (!authHeader || !authHeader.includes("Bearer")) {
28 | return res.render("index", { auth: false, error: false });
29 | }
30 |
31 | const token = authHeader.split(" ")[1];
32 |
33 | return res.render("index", {
34 | token,
35 | error: false
36 | });
37 | });
38 |
39 | // handle sign in/up form submit responses
40 | app.post("/", async (req, res, next) => {
41 | const token = req.query.token;
42 |
43 | if (!token) {
44 | return res.render("index", { auth: false, error: false });
45 | }
46 |
47 | try {
48 | const { data } = await axios.get(
49 | `http://${API_SERVICE_DNS}:${API_SERVICE_PORT}/api/v2/contacts?offset=1&limit=5`,
50 | {
51 | headers: { Authorization: `Bearer ${token}` }
52 | }
53 | );
54 |
55 | const {
56 | docs: { data: contacts },
57 | totalDocs,
58 | page,
59 | totalPages,
60 | hasNextPage,
61 | nextPage,
62 | hasPrevPage,
63 | prevPage,
64 | pagingCounter
65 | } = data;
66 |
67 | return res.render("index", {
68 | token,
69 | contacts: contacts.map(ct => ct.data),
70 | auth: true,
71 | error: false
72 | });
73 | } catch (error) {
74 | next(error);
75 | }
76 | });
77 |
78 | app.post("/signup", async (req, res) => {
79 | const { firstname, lastname, email, password, phone } = req.body;
80 |
81 | const user = {
82 | firstName: firstname,
83 | lastName: lastname,
84 | primaryEmailAddress: email,
85 | primaryContactNumber: phone,
86 | credential: { password }
87 | };
88 |
89 | try {
90 | const {
91 | data: { token }
92 | } = await axios.post(
93 | `http://${API_SERVICE_DNS}:${API_SERVICE_PORT}/auth/sign-up`,
94 | user
95 | );
96 |
97 | res.redirect(308, `/?token=${token}`);
98 | } catch (error) {
99 | next(error);
100 | }
101 | });
102 |
103 | app.post("/signin", async (req, res, next) => {
104 | const { email, password } = req.body;
105 |
106 | try {
107 | const {
108 | data: { token }
109 | } = await axios.post(
110 | `http://${API_SERVICE_DNS}:${API_SERVICE_PORT}/auth/sign-in`,
111 | {
112 | email,
113 | password
114 | }
115 | );
116 |
117 | res.redirect(308, `/?token=${token}`);
118 | } catch (error) {
119 | next(error);
120 | }
121 | });
122 |
123 | app.post("/add-contact", async (req, res, next) => {
124 | const token = req.query.token;
125 | const { firstname, lastname, email, phone } = req.body;
126 |
127 | const contact = {
128 | firstName: firstname,
129 | lastName: lastname,
130 | primaryEmailAddress: email,
131 | primaryContactNumber: phone
132 | };
133 |
134 | try {
135 | const { data } = await axios.post(
136 | `http://${API_SERVICE_DNS}:${API_SERVICE_PORT}/api/v2/contacts`,
137 | contact,
138 | {
139 | headers: { Authorization: `Bearer ${token}` }
140 | }
141 | );
142 |
143 | res.redirect(308, `/?token=${token}`);
144 | } catch (error) {
145 | next(error);
146 | }
147 | });
148 |
149 | app.use((err, req, res, next) => {
150 | res
151 | .status(err.status || 500)
152 | .render("index", { error: true, auth: false, errorMessage: err.message });
153 | });
154 |
155 | app.listen(PORT, () => {
156 | console.log(`Client server listening on port ${PORT}`);
157 | });
158 |
--------------------------------------------------------------------------------
/api/utils/format.utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Enable content negotiation
3 | * @param {{
4 | * res: Express.Response;
5 | * config: {
6 | * [field: string]: () => void
7 | * };
8 | * }} param0
9 | */
10 | export function contentNegotiator({ res, config }) {
11 | res.format(config);
12 | }
13 |
14 | /**
15 | * Convert array to CSV string
16 | * @param {any[]} arr
17 | * @returns {string}
18 | */
19 | export function convertToCSV(arr) {
20 | return [Object.keys(arr[0])] // field labels
21 | .concat(arr)
22 | .map(it => Object.values(it).toString())
23 | .join("\n");
24 | }
25 |
26 | /**
27 | *
28 | * @param {*} contact
29 | */
30 | export function orderContactProps(contact) {
31 | return Object.keys(contact)
32 | .sort()
33 | .reduce(
34 | (acc, key) => ({
35 | ...acc,
36 | [key]: contact[key]
37 | }),
38 | {
39 | _id: contact._id,
40 | firstName: contact.firstName,
41 | lastName: contact.lastName
42 | }
43 | );
44 | }
45 |
46 | /**
47 | *
48 | * @param {object} obj
49 | */
50 | export function getKeys(obj) {
51 | return Object.keys(obj);
52 | }
53 |
54 | /**
55 | *
56 | * @param {object} obj
57 | */
58 | export function getValues(obj) {
59 | return Object.values(obj);
60 | }
61 |
62 | /**
63 | * Return a POJO array from Mongoose Document array
64 | * @param {*} mongooseDocumentArray
65 | * @returns {object[]}
66 | */
67 | export function getPOJOArrayFrom(mongooseDocumentArray) {
68 | return mongooseDocumentArray.map(doc => doc.toObject());
69 | }
70 |
71 | /**
72 | * Return a contact POJO with ordered properties array from
73 | * Mongoose Document array of contacts
74 | * @param {*} contactsDocumentArray
75 | */
76 | export function getOrderedContactsPOJOArrayFrom(contactsDocumentArray) {
77 | return getPOJOArrayFrom(contactsDocumentArray).map(orderContactProps);
78 | }
79 |
80 | /**
81 | * Generate HTML table cells from object
82 | * @param {*} data
83 | */
84 | export function generateHTMLCells(data, style) {
85 | return getValues(data).reduce((acc, value) => {
86 | return acc.concat(
87 | "\n",
88 | `
89 |
90 | ${JSON.stringify(value, (k, v) => {
91 | // console.log(v);
92 | if (Array.isArray(v) && Object.keys(v[0]).length == 3 && v[0].name) {
93 | return v.map(
94 | s => `${s.name}`
95 | ).join(" ");
96 | }
97 | return k === "_id" ? undefined : v;
98 | })
99 | .replace(/"/g, "")
100 | .replace(/,/g, " ")
101 | .replace(/\{/g, "{ ")
102 | .replace(/\}/g, " }")
103 | .replace(/\[|\]/g, "")}
104 | |
105 | `
106 | );
107 | }, "");
108 | }
109 |
110 | /**
111 | * Generate HTML table headers from object
112 | * @param {*} data
113 | */
114 | export function generateHTMLTableHeaders(data, style) {
115 | return getKeys(data).reduce((acc, key) => {
116 | return acc.concat(
117 | "\n",
118 | `
119 |
120 | ${key}
121 | |
122 | `
123 | );
124 | }, "");
125 | }
126 |
127 | /**
128 | * Generate HTML table
129 | * @param {*} tableBody
130 | */
131 | export function generateHTMLTable(tableBody) {
132 | return `
133 |
134 |
135 |
136 |
156 |
157 |
158 |
159 |
160 |
161 | ${tableBody}
162 |
163 |
164 |
165 |
166 |
167 | `;
168 | }
169 |
--------------------------------------------------------------------------------
/api/routes/docs/v2/contactsV2.yaml:
--------------------------------------------------------------------------------
1 | tags:
2 | name: Contacts
3 | description: Contact management
4 | path:
5 | /contacts:
6 | get:
7 | summary: retrieve logged user contacts (paginated)
8 | tags: [Contacts]
9 | parameters:
10 | - in: header
11 | name: Authorization
12 | schema:
13 | type: string
14 | required: true
15 | - in: query
16 | name: offset
17 | schema:
18 | type: integer
19 | description: The number of pages to skip before starting to collect the result set
20 | - in: query
21 | name: limit
22 | schema:
23 | type: integer
24 | description: The numbers of items to return per page
25 | responses:
26 | "200":
27 | description: a page of contacts
28 | content:
29 | application/json:
30 | schema:
31 | # $ref:'#/components/schemas/Contact'
32 | type: object
33 | properties:
34 | docs:
35 | type: object
36 | properties:
37 | data:
38 | type: array
39 | items:
40 | type: object
41 | properties:
42 | data:
43 | type: object
44 | properties:
45 | id:
46 | type: string
47 | firstName:
48 | type: string
49 | lastName:
50 | type: string
51 | primaryContactNumber:
52 | type: string
53 | primaryEmailAddress:
54 | type: string
55 | image:
56 | type: string
57 | links:
58 | type: array
59 | items:
60 | type: object
61 | properties:
62 | href:
63 | type: string
64 | method:
65 | type: string
66 | rel:
67 | type: string
68 | totalDocs:
69 | type: integer
70 | page": 1,
71 | totalPages:
72 | type: integer
73 | hasNextPage:
74 | type: boolean
75 | nextPage:
76 | type: integer
77 | hasPrevPage:
78 | type: boolean
79 | prevPage:
80 | type: integer
81 | pagingCounter:
82 | type: integer
83 | "400":
84 | description: Bad request. User ID must be an integer and larger than 0.
85 | "401":
86 | description: Authorization information is missing or invalid.
87 | "404":
88 | description: A user with the specified ID was not found.
89 | "5XX":
90 | description: Unexpected error.
91 | post:
92 | summary: create contacts for logged user
93 | tags: [Contacts]
94 | parameters:
95 | - in: header
96 | name: Authorization
97 | schema:
98 | type: string
99 | required: true
100 | requestBody:
101 | description: contact to create
102 | required: true
103 | content:
104 | application/json:
105 | schema:
106 | type: object
107 | properties:
108 | firstName:
109 | type: string
110 | lastName:
111 | type: string
112 | primaryContactNumber:
113 | type: string
114 | primaryEmailAddress:
115 | type: string
116 | responses:
117 | "200":
118 | description: server feedback
119 | content:
120 | application/json:
121 | schema:
122 | type: object
123 | properties:
124 | message:
125 | type: boolean
126 |
--------------------------------------------------------------------------------
/api/controllers/contactsV1.js:
--------------------------------------------------------------------------------
1 | import { ObjectID } from "bson";
2 | import { errorHandler } from "../utils";
3 | import { Contact } from "../models";
4 | import { generateFakeContacts } from "../utils";
5 |
6 | export const getContacts = async (req, res) => {
7 | const contacts = await Contact.find()
8 | .populate("image")
9 | .exec();
10 | res.format({
11 | // using new object method syntax (instead of json: function() {...})
12 | json() {
13 | res.json(contacts);
14 | },
15 |
16 | text() {
17 | const contactsAstext = contacts
18 | .map(contact => Object.entries(contact).map(t => t.join(":")))
19 | .join("\n\n ==========================>> ");
20 | res.send(contactsAstext);
21 | },
22 |
23 | html() {
24 | const html = [
25 | ``,
26 | `| Contact ID |
27 | Contact Data |
28 | `
29 | ];
30 |
31 | contacts.forEach(({ _doc }) => {
32 | let { _id, ...contact } = _doc;
33 |
34 | // reorder properties
35 | contact = Object.keys(contact)
36 | .sort()
37 | .reduce(
38 | (acc, key) => ({
39 | ...acc,
40 | [key]: _doc[key]
41 | }),
42 | {
43 | firstName: _doc.firstName,
44 | lastName: _doc.lastName
45 | }
46 | );
47 |
48 | html.push(`
49 |
50 | | ${_id} |
51 | ${Object.entries(contact)
52 | .map(([key, value]) => {
53 | return `${key}: ${JSON.stringify(
54 | value,
55 | function replacer(k, v) {
56 | return k === "_id" ? undefined : v;
57 | },
58 | 4
59 | ).replace(/"/g, "")} `;
60 | })
61 | .join("\n")} |
62 |
63 | `);
64 | });
65 |
66 | res.send(html.join("\n"));
67 | }
68 | });
69 | };
70 |
71 | export const getContact = async (req, res, next) => {
72 | const contactId = req.params.id;
73 | contactId || next(errorHandler("Please enter a contact ID", 400));
74 | contactId.length >= 24 || next(errorHandler("Invalid contact ID", 422));
75 |
76 | const contact = await Contact.findOne({
77 | _id: new ObjectID(contactId)
78 | })
79 | .populate("image")
80 | .exec();
81 | res.json(contact);
82 | };
83 |
84 | export const postContact = async (req, res, next) => {
85 | const contact = req.body;
86 | contact || next(errorHandler("Please submit valid contact", 400));
87 | contact.primaryContactNumber ||
88 | next(errorHandler("Please submit valid contact", 422));
89 |
90 | const newContact = new Contact({ ...contact });
91 | const { _id, _doc } = await newContact.save();
92 |
93 | _doc && _doc.primaryContactNumber
94 | ? res
95 | .status(201)
96 | .set("location", `/contacts/${_id}`)
97 | .json({ message: "Contact created", data: _doc })
98 | : next(errorHandler("No contact created"));
99 | };
100 |
101 | export const postManyContacts = async (req, res, next) => {
102 | const n = parseInt(req.query.n);
103 | n < 100 || next(errorHandler("Please enter number less than 100", 422));
104 |
105 | const generatedContacts = await Contact.insertMany(generateFakeContacts(n));
106 |
107 | res.status(201).json({
108 | message: `${n} contacts generated`,
109 | locations: generatedContacts.map(({ _id }) => `/api/v1/contacts/${_id}`)
110 | });
111 | };
112 |
113 | export const putContact = async (req, res, next) => {
114 | const contactId = req.params.id;
115 | const contactUpdate = req.body;
116 |
117 | contactId || next(errorHandler("Please enter a contact ID", 400));
118 | contactUpdate || next(errorHandler("Please submit valid contact", 400));
119 |
120 | const result = await Contact.updateOne(
121 | { _id: new ObjectID(contactId) },
122 | { $set: contactUpdate }
123 | );
124 |
125 | result.nModified === 1
126 | ? res.json({ message: "Contact updated" })
127 | : next(errorHandler("No data updated"));
128 | };
129 |
130 | export const deleteContact = async (req, res, next) => {
131 | const contactId = req.params.id;
132 | contactId || next(errorHandler("Please enter a contact ID", 422));
133 |
134 | const result = await Contact.deleteOne({
135 | _id: new ObjectID(contactId)
136 | });
137 |
138 | result.deletedCount === 1
139 | ? res.json({ message: "Contact deleted" })
140 | : next(errorHandler("No data deleted"));
141 | };
142 |
143 | export const deleteAllContact = async (req, res, next) => {
144 | const result = await Contact.deleteMany({});
145 |
146 | result.deletedCount > 0
147 | ? res.json({ message: "All contacts deleted" })
148 | : next(errorHandler("No data deleted"));
149 | };
150 |
--------------------------------------------------------------------------------
/api/config/server.config.js:
--------------------------------------------------------------------------------
1 | import Express from "express";
2 | import cors from "cors";
3 | import helmet from "helmet";
4 | import morgan from "morgan";
5 | import paginate from "express-paginate";
6 |
7 | import DbConfig from "./db.config";
8 | import { ConfigService, CacheService } from "../services";
9 | import { RateLimiterConfig } from ".";
10 | import { AuthMiddleware } from "../middlewares";
11 |
12 | export default class ServerConfig {
13 | constructor({ port, middlewares, routers }) {
14 | this.app = Express();
15 | this.app.set("env", ConfigService.NODE_ENV);
16 | this.app.set("port", port);
17 | this.registerCORSMiddleware()
18 | .registerHelmetMiddleware()
19 | .registerRateLimiter()
20 | .registerMorganMiddleware()
21 | .registerJwtPassportMiddleware()
22 | .registerJSONMiddleware()
23 | .registerExpressPaginateMiddleware();
24 |
25 | middlewares &&
26 | middlewares.forEach(mdlw => {
27 | this.registerMiddleware(mdlw);
28 | });
29 |
30 | this.app.get("/ping", (req, res, next) => {
31 | res.send("pong");
32 | });
33 |
34 | routers &&
35 | routers.forEach(({ baseUrl, router }) => {
36 | this.registerRouter(baseUrl, router);
37 | });
38 |
39 | this.registerMiddleware(
40 | // catch 404 and forward to error handler
41 | function(req, res, next) {
42 | var err = new Error("Not Found");
43 | err.statusCode = 404;
44 | next(err);
45 | }
46 | );
47 | this.registerErrorHandlingMiddleware();
48 | }
49 |
50 | get port() {
51 | return this.app.get("port");
52 | }
53 |
54 | set port(number) {
55 | this.app.set("port", number);
56 | }
57 |
58 | /**
59 | * register any middleare
60 | * @param {*} middleware
61 | */
62 | registerMiddleware(middleware) {
63 | this.app.use(middleware);
64 | return this;
65 | }
66 |
67 | /**
68 | * register Express router
69 | * @param {*} router
70 | */
71 | registerRouter(baseUrl, router) {
72 | this.app.use(baseUrl, router);
73 | return this;
74 | }
75 |
76 | /**
77 | * register Express JSON middleware to handle requests with JSON body
78 | */
79 | registerJSONMiddleware() {
80 | this.registerMiddleware(Express.json());
81 | return this;
82 | }
83 |
84 | /**
85 | * register CORS middleware for cross origin requests
86 | */
87 | registerCORSMiddleware() {
88 | this.registerMiddleware(cors());
89 | return this;
90 | }
91 |
92 | /**
93 | * register Helmet middleware for Security HTTP headers
94 | */
95 | registerHelmetMiddleware() {
96 | this.app.use(helmet());
97 | return this;
98 | }
99 |
100 | /**
101 | * register Morgan middleware for request logging
102 | */
103 | registerMorganMiddleware() {
104 | this.registerMiddleware(morgan("combined"));
105 | return this;
106 | }
107 |
108 | /**
109 | * register Express Paginate middleware for pagianted data response
110 | */
111 | registerExpressPaginateMiddleware() {
112 | this.registerMiddleware(paginate.middleware(2, 100));
113 | return this;
114 | }
115 |
116 | /**
117 | * Register Rate Limiter middleware to prevent Denial of Service (DoS) attacks
118 | */
119 | registerRateLimiter() {
120 | // set global cache service
121 | global.redisCacheService = new CacheService({
122 | host: ConfigService.get("REDIS_HOST"),
123 | port: ConfigService.get("REDIS_PORT"),
124 | password: ConfigService.get("REDIS_PASSWORD")
125 | });
126 |
127 | const rateLimitConf = new RateLimiterConfig({
128 | client: global.redisCacheService.redisRateLimitClient,
129 | maxRequests: ConfigService.get("RATE_LIMIT_MAX_REQUESTS"),
130 | windowMs: ConfigService.get("RATE_LIMIT_WINDOW_MS")
131 | });
132 |
133 | const limiter = rateLimitConf.redisStoreLimiter;
134 |
135 | this.registerMiddleware(limiter);
136 | return this;
137 | }
138 |
139 | /**
140 | * register the Express Error Handling middleware
141 | */
142 | registerErrorHandlingMiddleware() {
143 | this.app.get("env") === "development"
144 | ? this.registerMiddleware(
145 | ({ statusCode = 500, message, stack }, req, res, next) => {
146 | res.status(statusCode);
147 | res.json({
148 | statusCode,
149 | message,
150 | stack
151 | });
152 | }
153 | )
154 | : this.registerMiddleware(
155 | ({ statusCode = 500, message }, req, res, next) => {
156 | res.status(statusCode);
157 | res.json({ statusCode, message });
158 | }
159 | );
160 | return this;
161 | }
162 |
163 | /**
164 | * register jwt Passport authentication middleware
165 | */
166 | registerJwtPassportMiddleware() {
167 | const authMdlw = new AuthMiddleware();
168 | const passportJwtMiddleware = authMdlw.registerJwtStrategy();
169 | this.registerMiddleware(passportJwtMiddleware);
170 | return this;
171 | }
172 |
173 | async listen() {
174 | try {
175 | const dbConf = new DbConfig("contactsdb", "images");
176 | await dbConf.connectDb();
177 |
178 | this.app.listen(this.port, () => {
179 | console.log(`Listening on port: ${this.port}`);
180 | });
181 | } catch (error) {
182 | console.error(`DB error: ${error.message}`);
183 | }
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/api/services/contacts.service.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | import { Contact, User } from "../models";
4 | import { fmtUtils, generateSelf, errorHandler } from "../utils";
5 | import DbConfig from "../config/db.config";
6 |
7 | import { ContactUtil } from "../utils";
8 | import { CacheService } from ".";
9 | import ConfigService from "./config.service";
10 |
11 | export default class ContactsService {
12 | #contactModel = Contact;
13 | #cacheService;
14 | #contactUtil;
15 | #gfsBucket;
16 |
17 | constructor() {}
18 |
19 | /**
20 | * Set dependencies that require open connection to database
21 | */
22 | setAsyncDependencies() {
23 | this.#cacheService = global.redisCacheService;
24 | this.#gfsBucket = DbConfig.gfsBucket;
25 |
26 | this.#contactUtil = new ContactUtil(
27 | this.#cacheService,
28 | this.#contactModel,
29 | this.#gfsBucket
30 | );
31 | }
32 |
33 | /**
34 | *
35 | * @param {object[]} contacts
36 | * @param {string} url
37 | */
38 | generateLinkedContacts(contacts, url) {
39 | return {
40 | data: contacts.map(contact => ({
41 | data: contact,
42 | links: generateSelf({
43 | entity: contact,
44 | url
45 | })
46 | })),
47 | links: generateSelf({ url })
48 | };
49 | }
50 |
51 | /**
52 | *
53 | * @param {object[]} contacts
54 | */
55 | generateHTMLContacts(contacts) {
56 | const orderedContactsPOJOArray = fmtUtils.getOrderedContactsPOJOArrayFrom(
57 | contacts
58 | );
59 |
60 | return `
61 | ${fmtUtils.generateHTMLTable(
62 | `
63 |
64 | ${fmtUtils.generateHTMLTableHeaders(orderedContactsPOJOArray[0], "")}
65 |
66 | ${orderedContactsPOJOArray.reduce((acc, contact) => {
67 | return acc.concat(
68 | "\n",
69 | `
70 |
71 | ${fmtUtils.generateHTMLCells(contact)}
72 |
73 | `
74 | );
75 | }, "")}
76 | `
77 | )}
78 | `;
79 | }
80 |
81 | /**
82 | *
83 | * @param {*} contacts
84 | */
85 | generateCSVContacts(contacts) {
86 | return fmtUtils.convertToCSV(contacts);
87 | }
88 |
89 | /**
90 | *
91 | * @param {*} contacts
92 | */
93 | generateTextContacts(contacts) {
94 | return contacts.reduce((acc, contact) => {
95 | return acc.concat(
96 | ",\n",
97 | JSON.stringify(contact, null, 4)
98 | .replace(/,/g, " ")
99 | .replace(/\{/g, "///#====================================\\ ")
100 | .replace(/\}/g, "\\====================================#///")
101 | );
102 | }, "");
103 | }
104 |
105 | /**
106 | *
107 | * @param {import("mongoose").Schema.Types.ObjectId} userId
108 | * @param {*} filter
109 | * @param {*} fields
110 | * @param {number} offset page to query
111 | * @param {number} limit max number of documents per page
112 | * @param {import("express").NextFunction} next
113 | */
114 | async findContacts(userId, filter = {}, fields, offset, limit, next) {
115 | // check if contacts for that page are already cached
116 | const cachedContactsPage = await this.#cacheService.getContactsForPage(
117 | userId,
118 | offset
119 | );
120 |
121 | console.log(
122 | `\npage: ${offset}, requested contacts per page: ${limit}, cached contacts: ${
123 | cachedContactsPage && cachedContactsPage.docs
124 | ? cachedContactsPage.docs.length
125 | : "no cache"
126 | }`
127 | );
128 |
129 | if (cachedContactsPage && cachedContactsPage.docs.length === limit) {
130 | console.log("========= using cache for contacts page ===============");
131 |
132 | return cachedContactsPage;
133 | } else {
134 | console.log("========= setting cache for contacts page ===============");
135 |
136 | await this.#cacheService.purgeCacheNow(userId);
137 |
138 | const options = {
139 | limit,
140 | page: offset,
141 | populate: "image",
142 | select: fields
143 | };
144 |
145 | const {
146 | docs,
147 | totalDocs,
148 | page,
149 | totalPages,
150 | hasNextPage,
151 | nextPage,
152 | hasPrevPage,
153 | prevPage,
154 | pagingCounter
155 | } = await this.#contactModel.paginate(filter, options);
156 |
157 | if (offset > totalPages)
158 | return next(errorHandler("Page number does not exist", 422));
159 |
160 | const contactsForPage = {
161 | docs,
162 | totalDocs,
163 | page,
164 | totalPages,
165 | hasNextPage,
166 | nextPage,
167 | hasPrevPage,
168 | prevPage,
169 | pagingCounter
170 | };
171 |
172 | // cache data for that page
173 | await this.#cacheService.storeContactsForPage(
174 | userId,
175 | contactsForPage,
176 | offset
177 | );
178 |
179 | return contactsForPage;
180 | }
181 | }
182 |
183 | /**
184 | *
185 | * @param {import("mongoose").Schema.Types.ObjectId} userId
186 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
187 | * @param {Express.Multer.File} file
188 | * @param {import("express").NextFunction} next
189 | */
190 | async attachContactImage(userId, contactId, file, next) {
191 | await this.#cacheService.purgeCacheNow(userId);
192 |
193 | // remove uploaded image = not be linked to contact
194 | if (!contactId || !isNaN(contactId)) {
195 | const orphanImageId = file.id;
196 |
197 | await this.#contactUtil.removeExistingImage(
198 | new mongoose.Types.ObjectId(orphanImageId)
199 | );
200 | return next(errorHandler("Please enter valid contact ID", 400));
201 | }
202 |
203 | const imageData = {
204 | image: file.id
205 | };
206 |
207 | // const contact = await contactsUtils.getContact(url, contactId, next);
208 | const contact = await this.#contactUtil.getContact(userId, contactId);
209 |
210 | // delete previous image if exists
211 | if (contact.image) {
212 | await this.#contactUtil.removeExistingImage(
213 | new mongoose.Types.ObjectId(contact.image._id),
214 | userId,
215 | contactId
216 | );
217 | }
218 |
219 | const updatedWithImage = await this.#contactUtil.updateContact(
220 | userId,
221 | contactId,
222 | imageData
223 | );
224 |
225 | return updatedWithImage;
226 | }
227 |
228 | /**
229 | *
230 | * @param {import("mongoose").Schema.Types.ObjectId} userId
231 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
232 | * @param {import("express").Response} res
233 | * @param {import("express").NextFunction} next
234 | */
235 | async getContactImage(userId, contactId, res, next) {
236 | if (!contactId)
237 | return next(errorHandler("Please enter valid contact ID", 400));
238 | if (!this.#gfsBucket) return next(errorHandler("No GridFS bucket"));
239 |
240 | try {
241 | const contact = await this.#contactUtil.getContact(userId, contactId);
242 |
243 | let imageId;
244 | contact.image
245 | ? (imageId = contact.image)
246 | : next(errorHandler("No image associated to this contact", 404));
247 |
248 | const imageObjectId = new mongoose.Types.ObjectId(imageId);
249 | const files = await this.#gfsBucket
250 | .find({ _id: imageObjectId })
251 | .toArray();
252 |
253 | if (!files || files.length === 0) {
254 | return next(errorHandler("no files exist", 404));
255 | }
256 |
257 | this.#gfsBucket.openDownloadStream(imageObjectId).pipe(res);
258 | } catch (error) {
259 | next(errorHandler(error.message));
260 | }
261 | }
262 |
263 | /**
264 | *
265 | * @param {import("mongoose").Schema.Types.ObjectId} userId
266 | * @param {import("mongoose").Schema.Types.ObjectId} contactId
267 | * @param {import("express").NextFunction} next
268 | */
269 | async deleteContactImage(userId, contactId, next) {
270 | await this.#cacheService.purgeCacheNow(userId);
271 |
272 | if (!contactId)
273 | return next(errorHandler("Please enter valid contact ID", 400));
274 | if (!this.#gfsBucket) return next(errorHandler("No GridFS bucket"));
275 |
276 | try {
277 | let imageId;
278 |
279 | // retrieve image id
280 | const contact = await this.#contactUtil.getContact(userId, contactId);
281 |
282 | contact.image
283 | ? (imageId = contact.image)
284 | : next(errorHandler("No image associated to this contact", 404));
285 |
286 | const removedImage = await this.#contactUtil.removeExistingImage(
287 | new mongoose.Types.ObjectId(imageId),
288 | userId,
289 | contactId
290 | );
291 |
292 | return removedImage;
293 | } catch (error) {
294 | next(errorHandler(error.message));
295 | }
296 | }
297 |
298 | /**
299 | * Create new contact and associate it to logged uqer
300 | * @param {*} userId
301 | * @param {*} contactData
302 | * @param {*} next
303 | */
304 | async postUserContact(userId, contactData, next) {
305 | const newContact = new Contact(contactData);
306 | const insertedContact = await newContact.save();
307 |
308 | const result = await User.updateOne(
309 | { _id: userId },
310 | {
311 | // $addToSet : operator to avoid duplicate values in array
312 | // can also use mongoose plugin https://www.npmjs.com/package/mongoose-unique-array
313 | $addToSet: { contacts: insertedContact._id }
314 | }
315 | ).select({
316 | firstName: 1,
317 | lastName: 1,
318 | primaryEmailAddress: 1,
319 | contacts: 1
320 | });
321 |
322 | return result.ok === 1 && result.nModified > 0;
323 | }
324 |
325 | /**
326 | *
327 | * @param {*} user
328 | * @param {*} contactId
329 | * @param {*} updateContactData
330 | * @param {*} next
331 | */
332 | async putUserContact(user, contactId, updateContactData, next) {
333 | // check contact id in logged user contacts
334 | const found = user.contacts.find(
335 | contact => contact.toString() === contactId
336 | );
337 |
338 | if (found) {
339 | const result = await Contact.updateOne(
340 | {
341 | _id: contactId
342 | },
343 | updateContactData
344 | );
345 |
346 | return result.ok === 1 && result.nModified > 0;
347 | }
348 |
349 | return false;
350 | }
351 |
352 | /**
353 | *
354 | * @param {*} user
355 | * @param {*} contactId
356 | * @param {*} next
357 | */
358 | async deleteUserContact(user, contactId, next) {
359 | // check contact id in logged user contacts
360 | const found = user.contacts.find(
361 | contact => contact.toString() === contactId
362 | );
363 |
364 | if (found) {
365 | const result = await Contact.deleteOne({
366 | _id: contactId
367 | });
368 |
369 | if (result.ok === 1 && result.deletedCount > 0) {
370 | // remove contact from user contacts
371 | user.contacts = user.contacts.filter(
372 | contact => contact.toString() !== contactId
373 | );
374 |
375 | const updated = await user.save();
376 |
377 | return Boolean(updated);
378 | }
379 | }
380 |
381 | return false;
382 | }
383 | }
384 |
--------------------------------------------------------------------------------
/client/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@opencensus/core": {
8 | "version": "0.0.9",
9 | "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz",
10 | "integrity": "sha512-31Q4VWtbzXpVUd2m9JS6HEaPjlKvNMOiF7lWKNmXF84yUcgfAFL5re7/hjDmdyQbOp32oGc+RFV78jXIldVz6Q==",
11 | "requires": {
12 | "continuation-local-storage": "^3.2.1",
13 | "log-driver": "^1.2.7",
14 | "semver": "^5.5.0",
15 | "shimmer": "^1.2.0",
16 | "uuid": "^3.2.1"
17 | }
18 | },
19 | "@opencensus/propagation-b3": {
20 | "version": "0.0.8",
21 | "resolved": "https://registry.npmjs.org/@opencensus/propagation-b3/-/propagation-b3-0.0.8.tgz",
22 | "integrity": "sha512-PffXX2AL8Sh0VHQ52jJC4u3T0H6wDK6N/4bg7xh4ngMYOIi13aR1kzVvX1sVDBgfGwDOkMbl4c54Xm3tlPx/+A==",
23 | "requires": {
24 | "@opencensus/core": "^0.0.8",
25 | "uuid": "^3.2.1"
26 | },
27 | "dependencies": {
28 | "@opencensus/core": {
29 | "version": "0.0.8",
30 | "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.8.tgz",
31 | "integrity": "sha512-yUFT59SFhGMYQgX0PhoTR0LBff2BEhPrD9io1jWfF/VDbakRfs6Pq60rjv0Z7iaTav5gQlttJCX2+VPxFWCuoQ==",
32 | "requires": {
33 | "continuation-local-storage": "^3.2.1",
34 | "log-driver": "^1.2.7",
35 | "semver": "^5.5.0",
36 | "shimmer": "^1.2.0",
37 | "uuid": "^3.2.1"
38 | }
39 | }
40 | }
41 | },
42 | "@pm2/agent": {
43 | "version": "0.5.26",
44 | "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-0.5.26.tgz",
45 | "integrity": "sha512-pqiS87IiUprkSR7SG0RKMATuYXl4QjH1tSSUwM4wJcovRT4pD5dvnnu61w9y/4/Ur5V/+a7bqS8bZz51y3U2iA==",
46 | "requires": {
47 | "async": "^2.6.0",
48 | "chalk": "^2.3.2",
49 | "eventemitter2": "^5.0.1",
50 | "fclone": "^1.0.11",
51 | "moment": "^2.21.0",
52 | "nssocket": "^0.6.0",
53 | "pm2-axon": "^3.2.0",
54 | "pm2-axon-rpc": "^0.5.0",
55 | "proxy-agent": "^3.1.0",
56 | "semver": "^5.5.0",
57 | "ws": "^5.1.0"
58 | },
59 | "dependencies": {
60 | "async": {
61 | "version": "2.6.3",
62 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
63 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
64 | "requires": {
65 | "lodash": "^4.17.14"
66 | }
67 | }
68 | }
69 | },
70 | "@pm2/agent-node": {
71 | "version": "1.1.10",
72 | "resolved": "https://registry.npmjs.org/@pm2/agent-node/-/agent-node-1.1.10.tgz",
73 | "integrity": "sha512-xRcrk7OEwhS3d/227/kKGvxgmbIi6Yyp27FzGlFNermEKhgddmFaRnmd7GRLIsBM/KB28NrwflBZulzk/mma6g==",
74 | "requires": {
75 | "debug": "^3.1.0",
76 | "eventemitter2": "^5.0.1",
77 | "proxy-agent": "^3.0.3",
78 | "ws": "^6.0.0"
79 | },
80 | "dependencies": {
81 | "debug": {
82 | "version": "3.2.6",
83 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
84 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
85 | "requires": {
86 | "ms": "^2.1.1"
87 | }
88 | },
89 | "ms": {
90 | "version": "2.1.2",
91 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
92 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
93 | },
94 | "ws": {
95 | "version": "6.2.1",
96 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
97 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
98 | "requires": {
99 | "async-limiter": "~1.0.0"
100 | }
101 | }
102 | }
103 | },
104 | "@pm2/io": {
105 | "version": "4.3.3",
106 | "resolved": "https://registry.npmjs.org/@pm2/io/-/io-4.3.3.tgz",
107 | "integrity": "sha512-ENGsdSVpnwbYMGdeB0/Xy2eZYo7oltzApoCsMD4ssqWNXDg9C4uQZy5J09iPsb0IHFwSDjU5oylXdwKDSoqODw==",
108 | "requires": {
109 | "@opencensus/core": "^0.0.9",
110 | "@opencensus/propagation-b3": "^0.0.8",
111 | "@pm2/agent-node": "^1.1.10",
112 | "async": "~2.6.1",
113 | "debug": "3.1.0",
114 | "eventemitter2": "~5.0.1",
115 | "require-in-the-middle": "^5.0.0",
116 | "semver": "5.5.0",
117 | "shimmer": "~1.2.0",
118 | "signal-exit": "3.0.2",
119 | "tslib": "1.9.3"
120 | },
121 | "dependencies": {
122 | "async": {
123 | "version": "2.6.3",
124 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
125 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
126 | "requires": {
127 | "lodash": "^4.17.14"
128 | }
129 | },
130 | "debug": {
131 | "version": "3.1.0",
132 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
133 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
134 | "requires": {
135 | "ms": "2.0.0"
136 | }
137 | },
138 | "semver": {
139 | "version": "5.5.0",
140 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
141 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
142 | }
143 | }
144 | },
145 | "@pm2/js-api": {
146 | "version": "0.5.60",
147 | "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.5.60.tgz",
148 | "integrity": "sha512-CvAbpIB7ObOuwvqhDBB/E4Z4ANRx2dBk08zYpGPNg+1fDj14FJg2e7DWA8bblSGNC8QarIXPaqPDJBL1e8cRQw==",
149 | "requires": {
150 | "async": "^2.4.1",
151 | "axios": "^0.19.0",
152 | "debug": "^2.6.8",
153 | "eventemitter2": "^4.1.0",
154 | "ws": "^3.0.0"
155 | },
156 | "dependencies": {
157 | "async": {
158 | "version": "2.6.3",
159 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
160 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
161 | "requires": {
162 | "lodash": "^4.17.14"
163 | }
164 | },
165 | "eventemitter2": {
166 | "version": "4.1.2",
167 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
168 | "integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU="
169 | },
170 | "ws": {
171 | "version": "3.3.3",
172 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
173 | "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
174 | "requires": {
175 | "async-limiter": "~1.0.0",
176 | "safe-buffer": "~5.1.0",
177 | "ultron": "~1.1.0"
178 | }
179 | }
180 | }
181 | },
182 | "@pm2/pm2-version-check": {
183 | "version": "1.0.3",
184 | "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.3.tgz",
185 | "integrity": "sha512-SBuYsh+o35knItbRW97vl5/5nEc5c5DYP7PxjyPLOfmm9bMaDsVeATXjXMBy6+KLlyrYWHZxGbfXe003NnHClg==",
186 | "requires": {
187 | "debug": "^4.1.1"
188 | },
189 | "dependencies": {
190 | "debug": {
191 | "version": "4.1.1",
192 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
193 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
194 | "requires": {
195 | "ms": "^2.1.1"
196 | }
197 | },
198 | "ms": {
199 | "version": "2.1.2",
200 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
201 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
202 | }
203 | }
204 | },
205 | "@types/axios": {
206 | "version": "0.14.0",
207 | "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
208 | "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
209 | "dev": true,
210 | "requires": {
211 | "axios": "*"
212 | }
213 | },
214 | "@types/body-parser": {
215 | "version": "1.19.0",
216 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
217 | "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
218 | "dev": true,
219 | "requires": {
220 | "@types/connect": "*",
221 | "@types/node": "*"
222 | }
223 | },
224 | "@types/connect": {
225 | "version": "3.4.33",
226 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
227 | "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
228 | "dev": true,
229 | "requires": {
230 | "@types/node": "*"
231 | }
232 | },
233 | "@types/cors": {
234 | "version": "2.8.6",
235 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz",
236 | "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==",
237 | "dev": true,
238 | "requires": {
239 | "@types/express": "*"
240 | }
241 | },
242 | "@types/ejs": {
243 | "version": "3.0.0",
244 | "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.0.0.tgz",
245 | "integrity": "sha512-6mUaeRO+zTcoFzjXZgYybf5/FtXXyWmfkGheS04iPOEmE+lV3qBkcqG0WnabmJD2ATSPTlWDBMUpxdE80JqUPQ==",
246 | "dev": true
247 | },
248 | "@types/express": {
249 | "version": "4.17.2",
250 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz",
251 | "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==",
252 | "dev": true,
253 | "requires": {
254 | "@types/body-parser": "*",
255 | "@types/express-serve-static-core": "*",
256 | "@types/serve-static": "*"
257 | }
258 | },
259 | "@types/express-serve-static-core": {
260 | "version": "4.17.2",
261 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz",
262 | "integrity": "sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==",
263 | "dev": true,
264 | "requires": {
265 | "@types/node": "*",
266 | "@types/range-parser": "*"
267 | }
268 | },
269 | "@types/helmet": {
270 | "version": "0.0.45",
271 | "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.45.tgz",
272 | "integrity": "sha512-PsLZI1NqKpXvsMZxh66xAZtpKiTeW+swY8a8LnCNSBbM/mvwU41P3BYoEqkJM9RbITPsq4uhIH0NkIsL9fzPbg==",
273 | "dev": true,
274 | "requires": {
275 | "@types/express": "*"
276 | }
277 | },
278 | "@types/mime": {
279 | "version": "2.0.1",
280 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
281 | "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
282 | "dev": true
283 | },
284 | "@types/morgan": {
285 | "version": "1.7.37",
286 | "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.37.tgz",
287 | "integrity": "sha512-tIdEA10BcHcOumMmUiiYdw8lhiVVq62r0ghih5Xpp4WETkfsMiTUZL4w9jCI502BBOrKhFrAOGml9IeELvVaBA==",
288 | "dev": true,
289 | "requires": {
290 | "@types/express": "*"
291 | }
292 | },
293 | "@types/node": {
294 | "version": "13.7.1",
295 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.1.tgz",
296 | "integrity": "sha512-Zq8gcQGmn4txQEJeiXo/KiLpon8TzAl0kmKH4zdWctPj05nWwp1ClMdAVEloqrQKfaC48PNLdgN/aVaLqUrluA==",
297 | "dev": true
298 | },
299 | "@types/range-parser": {
300 | "version": "1.2.3",
301 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
302 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
303 | "dev": true
304 | },
305 | "@types/serve-static": {
306 | "version": "1.13.3",
307 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
308 | "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
309 | "dev": true,
310 | "requires": {
311 | "@types/express-serve-static-core": "*",
312 | "@types/mime": "*"
313 | }
314 | },
315 | "accepts": {
316 | "version": "1.3.7",
317 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
318 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
319 | "requires": {
320 | "mime-types": "~2.1.24",
321 | "negotiator": "0.6.2"
322 | }
323 | },
324 | "agent-base": {
325 | "version": "4.3.0",
326 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
327 | "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
328 | "requires": {
329 | "es6-promisify": "^5.0.0"
330 | }
331 | },
332 | "amp": {
333 | "version": "0.3.1",
334 | "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
335 | "integrity": "sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0="
336 | },
337 | "amp-message": {
338 | "version": "0.1.2",
339 | "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz",
340 | "integrity": "sha1-p48cmJlQh602GSpBKY5NtJ49/EU=",
341 | "requires": {
342 | "amp": "0.3.1"
343 | }
344 | },
345 | "ansi-colors": {
346 | "version": "3.2.4",
347 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
348 | "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA=="
349 | },
350 | "ansi-regex": {
351 | "version": "2.1.1",
352 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
353 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
354 | },
355 | "ansi-styles": {
356 | "version": "3.2.1",
357 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
358 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
359 | "requires": {
360 | "color-convert": "^1.9.0"
361 | }
362 | },
363 | "anymatch": {
364 | "version": "3.1.1",
365 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
366 | "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
367 | "requires": {
368 | "normalize-path": "^3.0.0",
369 | "picomatch": "^2.0.4"
370 | }
371 | },
372 | "argparse": {
373 | "version": "1.0.10",
374 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
375 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
376 | "requires": {
377 | "sprintf-js": "~1.0.2"
378 | },
379 | "dependencies": {
380 | "sprintf-js": {
381 | "version": "1.0.3",
382 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
383 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
384 | }
385 | }
386 | },
387 | "array-flatten": {
388 | "version": "1.1.1",
389 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
390 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
391 | },
392 | "ast-types": {
393 | "version": "0.13.2",
394 | "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.2.tgz",
395 | "integrity": "sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA=="
396 | },
397 | "async": {
398 | "version": "3.1.1",
399 | "resolved": "https://registry.npmjs.org/async/-/async-3.1.1.tgz",
400 | "integrity": "sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g=="
401 | },
402 | "async-limiter": {
403 | "version": "1.0.1",
404 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
405 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
406 | },
407 | "async-listener": {
408 | "version": "0.6.10",
409 | "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",
410 | "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==",
411 | "requires": {
412 | "semver": "^5.3.0",
413 | "shimmer": "^1.1.0"
414 | }
415 | },
416 | "axios": {
417 | "version": "0.19.2",
418 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
419 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
420 | "requires": {
421 | "follow-redirects": "1.5.10"
422 | }
423 | },
424 | "balanced-match": {
425 | "version": "1.0.0",
426 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
427 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
428 | },
429 | "basic-auth": {
430 | "version": "2.0.1",
431 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
432 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
433 | "requires": {
434 | "safe-buffer": "5.1.2"
435 | }
436 | },
437 | "binary-extensions": {
438 | "version": "2.0.0",
439 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
440 | "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow=="
441 | },
442 | "blessed": {
443 | "version": "0.1.81",
444 | "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz",
445 | "integrity": "sha1-+WLWh+wsNpVwrnGvhDJW5tDKESk="
446 | },
447 | "bodec": {
448 | "version": "0.1.0",
449 | "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz",
450 | "integrity": "sha1-vIUVVUMPI8n3ZQp172TGqUw0GMw="
451 | },
452 | "body-parser": {
453 | "version": "1.19.0",
454 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
455 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
456 | "requires": {
457 | "bytes": "3.1.0",
458 | "content-type": "~1.0.4",
459 | "debug": "2.6.9",
460 | "depd": "~1.1.2",
461 | "http-errors": "1.7.2",
462 | "iconv-lite": "0.4.24",
463 | "on-finished": "~2.3.0",
464 | "qs": "6.7.0",
465 | "raw-body": "2.4.0",
466 | "type-is": "~1.6.17"
467 | }
468 | },
469 | "bowser": {
470 | "version": "2.9.0",
471 | "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
472 | "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA=="
473 | },
474 | "brace-expansion": {
475 | "version": "1.1.11",
476 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
477 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
478 | "requires": {
479 | "balanced-match": "^1.0.0",
480 | "concat-map": "0.0.1"
481 | }
482 | },
483 | "braces": {
484 | "version": "3.0.2",
485 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
486 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
487 | "requires": {
488 | "fill-range": "^7.0.1"
489 | }
490 | },
491 | "buffer-from": {
492 | "version": "1.1.1",
493 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
494 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
495 | },
496 | "bytes": {
497 | "version": "3.1.0",
498 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
499 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
500 | },
501 | "camelize": {
502 | "version": "1.0.0",
503 | "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
504 | "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
505 | },
506 | "chalk": {
507 | "version": "2.4.2",
508 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
509 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
510 | "requires": {
511 | "ansi-styles": "^3.2.1",
512 | "escape-string-regexp": "^1.0.5",
513 | "supports-color": "^5.3.0"
514 | }
515 | },
516 | "charm": {
517 | "version": "0.1.2",
518 | "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz",
519 | "integrity": "sha1-BsIe7RobBq62dVPNxT4jJ0usIpY="
520 | },
521 | "chokidar": {
522 | "version": "3.3.1",
523 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
524 | "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
525 | "requires": {
526 | "anymatch": "~3.1.1",
527 | "braces": "~3.0.2",
528 | "fsevents": "~2.1.2",
529 | "glob-parent": "~5.1.0",
530 | "is-binary-path": "~2.1.0",
531 | "is-glob": "~4.0.1",
532 | "normalize-path": "~3.0.0",
533 | "readdirp": "~3.3.0"
534 | }
535 | },
536 | "cli-table-redemption": {
537 | "version": "1.0.1",
538 | "resolved": "https://registry.npmjs.org/cli-table-redemption/-/cli-table-redemption-1.0.1.tgz",
539 | "integrity": "sha512-SjVCciRyx01I4azo2K2rcc0NP/wOceXGzG1ZpYkEulbbIxDA/5YWv0oxG2HtQ4v8zPC6bgbRI7SbNaTZCxMNkg==",
540 | "requires": {
541 | "chalk": "^1.1.3"
542 | },
543 | "dependencies": {
544 | "ansi-styles": {
545 | "version": "2.2.1",
546 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
547 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
548 | },
549 | "chalk": {
550 | "version": "1.1.3",
551 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
552 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
553 | "requires": {
554 | "ansi-styles": "^2.2.1",
555 | "escape-string-regexp": "^1.0.2",
556 | "has-ansi": "^2.0.0",
557 | "strip-ansi": "^3.0.0",
558 | "supports-color": "^2.0.0"
559 | }
560 | },
561 | "supports-color": {
562 | "version": "2.0.0",
563 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
564 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
565 | }
566 | }
567 | },
568 | "co": {
569 | "version": "4.6.0",
570 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
571 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
572 | },
573 | "color-convert": {
574 | "version": "1.9.3",
575 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
576 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
577 | "requires": {
578 | "color-name": "1.1.3"
579 | }
580 | },
581 | "color-name": {
582 | "version": "1.1.3",
583 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
584 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
585 | },
586 | "commander": {
587 | "version": "2.15.1",
588 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
589 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
590 | },
591 | "concat-map": {
592 | "version": "0.0.1",
593 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
594 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
595 | },
596 | "content-disposition": {
597 | "version": "0.5.3",
598 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
599 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
600 | "requires": {
601 | "safe-buffer": "5.1.2"
602 | }
603 | },
604 | "content-security-policy-builder": {
605 | "version": "2.1.0",
606 | "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz",
607 | "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ=="
608 | },
609 | "content-type": {
610 | "version": "1.0.4",
611 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
612 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
613 | },
614 | "continuation-local-storage": {
615 | "version": "3.2.1",
616 | "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
617 | "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==",
618 | "requires": {
619 | "async-listener": "^0.6.0",
620 | "emitter-listener": "^1.1.1"
621 | }
622 | },
623 | "cookie": {
624 | "version": "0.4.0",
625 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
626 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
627 | },
628 | "cookie-signature": {
629 | "version": "1.0.6",
630 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
631 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
632 | },
633 | "core-util-is": {
634 | "version": "1.0.2",
635 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
636 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
637 | },
638 | "cors": {
639 | "version": "2.8.5",
640 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
641 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
642 | "requires": {
643 | "object-assign": "^4",
644 | "vary": "^1"
645 | }
646 | },
647 | "cron": {
648 | "version": "1.7.1",
649 | "resolved": "https://registry.npmjs.org/cron/-/cron-1.7.1.tgz",
650 | "integrity": "sha512-gmMB/pJcqUVs/NklR1sCGlNYM7TizEw+1gebz20BMc/8bTm/r7QUp3ZPSPlG8Z5XRlvb7qhjEjq/+bdIfUCL2A==",
651 | "requires": {
652 | "moment-timezone": "^0.5.x"
653 | }
654 | },
655 | "culvert": {
656 | "version": "0.1.2",
657 | "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz",
658 | "integrity": "sha1-lQL18BVKLVoioCPnn3HMk2+m728="
659 | },
660 | "dasherize": {
661 | "version": "2.0.0",
662 | "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz",
663 | "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg="
664 | },
665 | "data-uri-to-buffer": {
666 | "version": "1.2.0",
667 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz",
668 | "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ=="
669 | },
670 | "date-fns": {
671 | "version": "1.30.1",
672 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
673 | "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw=="
674 | },
675 | "debug": {
676 | "version": "2.6.9",
677 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
678 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
679 | "requires": {
680 | "ms": "2.0.0"
681 | }
682 | },
683 | "deep-is": {
684 | "version": "0.1.3",
685 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
686 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
687 | },
688 | "degenerator": {
689 | "version": "1.0.4",
690 | "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz",
691 | "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=",
692 | "requires": {
693 | "ast-types": "0.x.x",
694 | "escodegen": "1.x.x",
695 | "esprima": "3.x.x"
696 | }
697 | },
698 | "depd": {
699 | "version": "1.1.2",
700 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
701 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
702 | },
703 | "destroy": {
704 | "version": "1.0.4",
705 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
706 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
707 | },
708 | "dns-prefetch-control": {
709 | "version": "0.2.0",
710 | "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz",
711 | "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q=="
712 | },
713 | "dont-sniff-mimetype": {
714 | "version": "1.1.0",
715 | "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz",
716 | "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug=="
717 | },
718 | "ee-first": {
719 | "version": "1.1.1",
720 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
721 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
722 | },
723 | "ejs": {
724 | "version": "3.0.1",
725 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.0.1.tgz",
726 | "integrity": "sha512-cuIMtJwxvzumSAkqaaoGY/L6Fc/t6YvoP9/VIaK0V/CyqKLEQ8sqODmYfy/cjXEdZ9+OOL8TecbJu+1RsofGDw=="
727 | },
728 | "emitter-listener": {
729 | "version": "1.1.2",
730 | "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
731 | "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
732 | "requires": {
733 | "shimmer": "^1.2.0"
734 | }
735 | },
736 | "encodeurl": {
737 | "version": "1.0.2",
738 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
739 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
740 | },
741 | "enquirer": {
742 | "version": "2.3.4",
743 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.4.tgz",
744 | "integrity": "sha512-pkYrrDZumL2VS6VBGDhqbajCM2xpkUNLuKfGPjfKaSIBKYopQbqEFyrOkRMIb2HDR/rO1kGhEt/5twBwtzKBXw==",
745 | "requires": {
746 | "ansi-colors": "^3.2.1"
747 | }
748 | },
749 | "es6-promise": {
750 | "version": "4.2.8",
751 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
752 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
753 | },
754 | "es6-promisify": {
755 | "version": "5.0.0",
756 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
757 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
758 | "requires": {
759 | "es6-promise": "^4.0.3"
760 | }
761 | },
762 | "escape-html": {
763 | "version": "1.0.3",
764 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
765 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
766 | },
767 | "escape-regexp": {
768 | "version": "0.0.1",
769 | "resolved": "https://registry.npmjs.org/escape-regexp/-/escape-regexp-0.0.1.tgz",
770 | "integrity": "sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ="
771 | },
772 | "escape-string-regexp": {
773 | "version": "1.0.5",
774 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
775 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
776 | },
777 | "escodegen": {
778 | "version": "1.14.1",
779 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
780 | "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
781 | "requires": {
782 | "esprima": "^4.0.1",
783 | "estraverse": "^4.2.0",
784 | "esutils": "^2.0.2",
785 | "optionator": "^0.8.1",
786 | "source-map": "~0.6.1"
787 | },
788 | "dependencies": {
789 | "esprima": {
790 | "version": "4.0.1",
791 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
792 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
793 | }
794 | }
795 | },
796 | "esprima": {
797 | "version": "3.1.3",
798 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
799 | "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
800 | },
801 | "estraverse": {
802 | "version": "4.3.0",
803 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
804 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
805 | },
806 | "esutils": {
807 | "version": "2.0.3",
808 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
809 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
810 | },
811 | "etag": {
812 | "version": "1.8.1",
813 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
814 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
815 | },
816 | "eventemitter2": {
817 | "version": "5.0.1",
818 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
819 | "integrity": "sha1-YZegldX7a1folC9v1+qtY6CclFI="
820 | },
821 | "expect-ct": {
822 | "version": "0.2.0",
823 | "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz",
824 | "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
825 | },
826 | "express": {
827 | "version": "4.17.1",
828 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
829 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
830 | "requires": {
831 | "accepts": "~1.3.7",
832 | "array-flatten": "1.1.1",
833 | "body-parser": "1.19.0",
834 | "content-disposition": "0.5.3",
835 | "content-type": "~1.0.4",
836 | "cookie": "0.4.0",
837 | "cookie-signature": "1.0.6",
838 | "debug": "2.6.9",
839 | "depd": "~1.1.2",
840 | "encodeurl": "~1.0.2",
841 | "escape-html": "~1.0.3",
842 | "etag": "~1.8.1",
843 | "finalhandler": "~1.1.2",
844 | "fresh": "0.5.2",
845 | "merge-descriptors": "1.0.1",
846 | "methods": "~1.1.2",
847 | "on-finished": "~2.3.0",
848 | "parseurl": "~1.3.3",
849 | "path-to-regexp": "0.1.7",
850 | "proxy-addr": "~2.0.5",
851 | "qs": "6.7.0",
852 | "range-parser": "~1.2.1",
853 | "safe-buffer": "5.1.2",
854 | "send": "0.17.1",
855 | "serve-static": "1.14.1",
856 | "setprototypeof": "1.1.1",
857 | "statuses": "~1.5.0",
858 | "type-is": "~1.6.18",
859 | "utils-merge": "1.0.1",
860 | "vary": "~1.1.2"
861 | }
862 | },
863 | "extend": {
864 | "version": "3.0.2",
865 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
866 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
867 | },
868 | "fast-levenshtein": {
869 | "version": "2.0.6",
870 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
871 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
872 | },
873 | "fclone": {
874 | "version": "1.0.11",
875 | "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz",
876 | "integrity": "sha1-EOhdo4v+p/xZk0HClu4ddyZu5kA="
877 | },
878 | "feature-policy": {
879 | "version": "0.3.0",
880 | "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz",
881 | "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ=="
882 | },
883 | "file-uri-to-path": {
884 | "version": "1.0.0",
885 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
886 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
887 | },
888 | "fill-range": {
889 | "version": "7.0.1",
890 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
891 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
892 | "requires": {
893 | "to-regex-range": "^5.0.1"
894 | }
895 | },
896 | "finalhandler": {
897 | "version": "1.1.2",
898 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
899 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
900 | "requires": {
901 | "debug": "2.6.9",
902 | "encodeurl": "~1.0.2",
903 | "escape-html": "~1.0.3",
904 | "on-finished": "~2.3.0",
905 | "parseurl": "~1.3.3",
906 | "statuses": "~1.5.0",
907 | "unpipe": "~1.0.0"
908 | }
909 | },
910 | "follow-redirects": {
911 | "version": "1.5.10",
912 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
913 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
914 | "requires": {
915 | "debug": "=3.1.0"
916 | },
917 | "dependencies": {
918 | "debug": {
919 | "version": "3.1.0",
920 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
921 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
922 | "requires": {
923 | "ms": "2.0.0"
924 | }
925 | }
926 | }
927 | },
928 | "forwarded": {
929 | "version": "0.1.2",
930 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
931 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
932 | },
933 | "frameguard": {
934 | "version": "3.1.0",
935 | "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz",
936 | "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g=="
937 | },
938 | "fresh": {
939 | "version": "0.5.2",
940 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
941 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
942 | },
943 | "fs.realpath": {
944 | "version": "1.0.0",
945 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
946 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
947 | },
948 | "fsevents": {
949 | "version": "2.1.2",
950 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
951 | "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
952 | "optional": true
953 | },
954 | "ftp": {
955 | "version": "0.3.10",
956 | "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz",
957 | "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=",
958 | "requires": {
959 | "readable-stream": "1.1.x",
960 | "xregexp": "2.0.0"
961 | },
962 | "dependencies": {
963 | "readable-stream": {
964 | "version": "1.1.14",
965 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
966 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
967 | "requires": {
968 | "core-util-is": "~1.0.0",
969 | "inherits": "~2.0.1",
970 | "isarray": "0.0.1",
971 | "string_decoder": "~0.10.x"
972 | }
973 | }
974 | }
975 | },
976 | "get-uri": {
977 | "version": "2.0.4",
978 | "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz",
979 | "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==",
980 | "requires": {
981 | "data-uri-to-buffer": "1",
982 | "debug": "2",
983 | "extend": "~3.0.2",
984 | "file-uri-to-path": "1",
985 | "ftp": "~0.3.10",
986 | "readable-stream": "2"
987 | }
988 | },
989 | "git-node-fs": {
990 | "version": "1.0.0",
991 | "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz",
992 | "integrity": "sha1-SbIV4kLr5Dqkx1Ybu6SZUhdSCA8="
993 | },
994 | "git-sha1": {
995 | "version": "0.1.2",
996 | "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz",
997 | "integrity": "sha1-WZrBkrcYdYJeE6RF86bgURjC90U="
998 | },
999 | "glob": {
1000 | "version": "7.1.6",
1001 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
1002 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
1003 | "requires": {
1004 | "fs.realpath": "^1.0.0",
1005 | "inflight": "^1.0.4",
1006 | "inherits": "2",
1007 | "minimatch": "^3.0.4",
1008 | "once": "^1.3.0",
1009 | "path-is-absolute": "^1.0.0"
1010 | }
1011 | },
1012 | "glob-parent": {
1013 | "version": "5.1.0",
1014 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
1015 | "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
1016 | "requires": {
1017 | "is-glob": "^4.0.1"
1018 | }
1019 | },
1020 | "has-ansi": {
1021 | "version": "2.0.0",
1022 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
1023 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
1024 | "requires": {
1025 | "ansi-regex": "^2.0.0"
1026 | }
1027 | },
1028 | "has-flag": {
1029 | "version": "3.0.0",
1030 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
1031 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
1032 | },
1033 | "helmet": {
1034 | "version": "3.21.2",
1035 | "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.21.2.tgz",
1036 | "integrity": "sha512-okUo+MeWgg00cKB8Csblu8EXgcIoDyb5ZS/3u0W4spCimeVuCUvVZ6Vj3O2VJ1Sxpyb8jCDvzu0L1KKT11pkIg==",
1037 | "requires": {
1038 | "depd": "2.0.0",
1039 | "dns-prefetch-control": "0.2.0",
1040 | "dont-sniff-mimetype": "1.1.0",
1041 | "expect-ct": "0.2.0",
1042 | "feature-policy": "0.3.0",
1043 | "frameguard": "3.1.0",
1044 | "helmet-crossdomain": "0.4.0",
1045 | "helmet-csp": "2.9.4",
1046 | "hide-powered-by": "1.1.0",
1047 | "hpkp": "2.0.0",
1048 | "hsts": "2.2.0",
1049 | "ienoopen": "1.1.0",
1050 | "nocache": "2.1.0",
1051 | "referrer-policy": "1.2.0",
1052 | "x-xss-protection": "1.3.0"
1053 | },
1054 | "dependencies": {
1055 | "depd": {
1056 | "version": "2.0.0",
1057 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
1058 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
1059 | }
1060 | }
1061 | },
1062 | "helmet-crossdomain": {
1063 | "version": "0.4.0",
1064 | "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz",
1065 | "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA=="
1066 | },
1067 | "helmet-csp": {
1068 | "version": "2.9.4",
1069 | "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.9.4.tgz",
1070 | "integrity": "sha512-qUgGx8+yk7Xl8XFEGI4MFu1oNmulxhQVTlV8HP8tV3tpfslCs30OZz/9uQqsWPvDISiu/NwrrCowsZBhFADYqg==",
1071 | "requires": {
1072 | "bowser": "^2.7.0",
1073 | "camelize": "1.0.0",
1074 | "content-security-policy-builder": "2.1.0",
1075 | "dasherize": "2.0.0"
1076 | }
1077 | },
1078 | "hide-powered-by": {
1079 | "version": "1.1.0",
1080 | "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz",
1081 | "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg=="
1082 | },
1083 | "hpkp": {
1084 | "version": "2.0.0",
1085 | "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz",
1086 | "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI="
1087 | },
1088 | "hsts": {
1089 | "version": "2.2.0",
1090 | "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz",
1091 | "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==",
1092 | "requires": {
1093 | "depd": "2.0.0"
1094 | },
1095 | "dependencies": {
1096 | "depd": {
1097 | "version": "2.0.0",
1098 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
1099 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
1100 | }
1101 | }
1102 | },
1103 | "http-errors": {
1104 | "version": "1.7.2",
1105 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
1106 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
1107 | "requires": {
1108 | "depd": "~1.1.2",
1109 | "inherits": "2.0.3",
1110 | "setprototypeof": "1.1.1",
1111 | "statuses": ">= 1.5.0 < 2",
1112 | "toidentifier": "1.0.0"
1113 | }
1114 | },
1115 | "http-proxy-agent": {
1116 | "version": "2.1.0",
1117 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
1118 | "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
1119 | "requires": {
1120 | "agent-base": "4",
1121 | "debug": "3.1.0"
1122 | },
1123 | "dependencies": {
1124 | "debug": {
1125 | "version": "3.1.0",
1126 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
1127 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
1128 | "requires": {
1129 | "ms": "2.0.0"
1130 | }
1131 | }
1132 | }
1133 | },
1134 | "https-proxy-agent": {
1135 | "version": "3.0.1",
1136 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
1137 | "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
1138 | "requires": {
1139 | "agent-base": "^4.3.0",
1140 | "debug": "^3.1.0"
1141 | },
1142 | "dependencies": {
1143 | "debug": {
1144 | "version": "3.2.6",
1145 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
1146 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
1147 | "requires": {
1148 | "ms": "^2.1.1"
1149 | }
1150 | },
1151 | "ms": {
1152 | "version": "2.1.2",
1153 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1154 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1155 | }
1156 | }
1157 | },
1158 | "iconv-lite": {
1159 | "version": "0.4.24",
1160 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1161 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1162 | "requires": {
1163 | "safer-buffer": ">= 2.1.2 < 3"
1164 | }
1165 | },
1166 | "ienoopen": {
1167 | "version": "1.1.0",
1168 | "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz",
1169 | "integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ=="
1170 | },
1171 | "inflight": {
1172 | "version": "1.0.6",
1173 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1174 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
1175 | "requires": {
1176 | "once": "^1.3.0",
1177 | "wrappy": "1"
1178 | }
1179 | },
1180 | "inherits": {
1181 | "version": "2.0.3",
1182 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
1183 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
1184 | },
1185 | "ini": {
1186 | "version": "1.3.5",
1187 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
1188 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
1189 | },
1190 | "interpret": {
1191 | "version": "1.2.0",
1192 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
1193 | "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw=="
1194 | },
1195 | "ip": {
1196 | "version": "1.1.5",
1197 | "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
1198 | "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
1199 | },
1200 | "ipaddr.js": {
1201 | "version": "1.9.0",
1202 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
1203 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
1204 | },
1205 | "is-binary-path": {
1206 | "version": "2.1.0",
1207 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1208 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1209 | "requires": {
1210 | "binary-extensions": "^2.0.0"
1211 | }
1212 | },
1213 | "is-extglob": {
1214 | "version": "2.1.1",
1215 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1216 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
1217 | },
1218 | "is-glob": {
1219 | "version": "4.0.1",
1220 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
1221 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
1222 | "requires": {
1223 | "is-extglob": "^2.1.1"
1224 | }
1225 | },
1226 | "is-number": {
1227 | "version": "7.0.0",
1228 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1229 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
1230 | },
1231 | "isarray": {
1232 | "version": "0.0.1",
1233 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
1234 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
1235 | },
1236 | "js-git": {
1237 | "version": "0.7.8",
1238 | "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz",
1239 | "integrity": "sha1-UvplWrYYd9bxB578ZTS1VPMeVEQ=",
1240 | "requires": {
1241 | "bodec": "^0.1.0",
1242 | "culvert": "^0.1.2",
1243 | "git-sha1": "^0.1.2",
1244 | "pako": "^0.2.5"
1245 | }
1246 | },
1247 | "lazy": {
1248 | "version": "1.0.11",
1249 | "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz",
1250 | "integrity": "sha1-2qBoIGKCVCwIgojpdcKXwa53tpA="
1251 | },
1252 | "levn": {
1253 | "version": "0.3.0",
1254 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
1255 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
1256 | "requires": {
1257 | "prelude-ls": "~1.1.2",
1258 | "type-check": "~0.3.2"
1259 | }
1260 | },
1261 | "lodash": {
1262 | "version": "4.17.14",
1263 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
1264 | "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
1265 | },
1266 | "lodash.findindex": {
1267 | "version": "4.6.0",
1268 | "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz",
1269 | "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY="
1270 | },
1271 | "lodash.foreach": {
1272 | "version": "4.5.0",
1273 | "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
1274 | "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
1275 | },
1276 | "lodash.get": {
1277 | "version": "4.4.2",
1278 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
1279 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
1280 | },
1281 | "lodash.last": {
1282 | "version": "3.0.0",
1283 | "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz",
1284 | "integrity": "sha1-JC9mMRLdTG5jcoxgo8kJ0b2tvUw="
1285 | },
1286 | "log-driver": {
1287 | "version": "1.2.7",
1288 | "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
1289 | "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg=="
1290 | },
1291 | "lru-cache": {
1292 | "version": "5.1.1",
1293 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1294 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1295 | "requires": {
1296 | "yallist": "^3.0.2"
1297 | }
1298 | },
1299 | "media-typer": {
1300 | "version": "0.3.0",
1301 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1302 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
1303 | },
1304 | "merge-descriptors": {
1305 | "version": "1.0.1",
1306 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
1307 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
1308 | },
1309 | "methods": {
1310 | "version": "1.1.2",
1311 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1312 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
1313 | },
1314 | "mime": {
1315 | "version": "1.6.0",
1316 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1317 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
1318 | },
1319 | "mime-db": {
1320 | "version": "1.43.0",
1321 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
1322 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
1323 | },
1324 | "mime-types": {
1325 | "version": "2.1.26",
1326 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
1327 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
1328 | "requires": {
1329 | "mime-db": "1.43.0"
1330 | }
1331 | },
1332 | "minimatch": {
1333 | "version": "3.0.4",
1334 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
1335 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
1336 | "requires": {
1337 | "brace-expansion": "^1.1.7"
1338 | }
1339 | },
1340 | "minimist": {
1341 | "version": "0.0.8",
1342 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
1343 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
1344 | },
1345 | "mkdirp": {
1346 | "version": "0.5.1",
1347 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
1348 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
1349 | "requires": {
1350 | "minimist": "0.0.8"
1351 | }
1352 | },
1353 | "module-details-from-path": {
1354 | "version": "1.0.3",
1355 | "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
1356 | "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
1357 | },
1358 | "moment": {
1359 | "version": "2.24.0",
1360 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
1361 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
1362 | },
1363 | "moment-timezone": {
1364 | "version": "0.5.27",
1365 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz",
1366 | "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==",
1367 | "requires": {
1368 | "moment": ">= 2.9.0"
1369 | }
1370 | },
1371 | "morgan": {
1372 | "version": "1.9.1",
1373 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
1374 | "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
1375 | "requires": {
1376 | "basic-auth": "~2.0.0",
1377 | "debug": "2.6.9",
1378 | "depd": "~1.1.2",
1379 | "on-finished": "~2.3.0",
1380 | "on-headers": "~1.0.1"
1381 | }
1382 | },
1383 | "ms": {
1384 | "version": "2.0.0",
1385 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1386 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
1387 | },
1388 | "mute-stream": {
1389 | "version": "0.0.8",
1390 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
1391 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
1392 | },
1393 | "needle": {
1394 | "version": "2.4.0",
1395 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
1396 | "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
1397 | "requires": {
1398 | "debug": "^3.2.6",
1399 | "iconv-lite": "^0.4.4",
1400 | "sax": "^1.2.4"
1401 | },
1402 | "dependencies": {
1403 | "debug": {
1404 | "version": "3.2.6",
1405 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
1406 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
1407 | "requires": {
1408 | "ms": "^2.1.1"
1409 | }
1410 | },
1411 | "ms": {
1412 | "version": "2.1.2",
1413 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1414 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1415 | }
1416 | }
1417 | },
1418 | "negotiator": {
1419 | "version": "0.6.2",
1420 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
1421 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
1422 | },
1423 | "netmask": {
1424 | "version": "1.0.6",
1425 | "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
1426 | "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU="
1427 | },
1428 | "nocache": {
1429 | "version": "2.1.0",
1430 | "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
1431 | "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
1432 | },
1433 | "normalize-path": {
1434 | "version": "3.0.0",
1435 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1436 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
1437 | },
1438 | "nssocket": {
1439 | "version": "0.6.0",
1440 | "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz",
1441 | "integrity": "sha1-Wflvb/MhVm8zxw99vu7N/cBxVPo=",
1442 | "requires": {
1443 | "eventemitter2": "~0.4.14",
1444 | "lazy": "~1.0.11"
1445 | },
1446 | "dependencies": {
1447 | "eventemitter2": {
1448 | "version": "0.4.14",
1449 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
1450 | "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas="
1451 | }
1452 | }
1453 | },
1454 | "object-assign": {
1455 | "version": "4.1.1",
1456 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1457 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
1458 | },
1459 | "on-finished": {
1460 | "version": "2.3.0",
1461 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
1462 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
1463 | "requires": {
1464 | "ee-first": "1.1.1"
1465 | }
1466 | },
1467 | "on-headers": {
1468 | "version": "1.0.2",
1469 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
1470 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
1471 | },
1472 | "once": {
1473 | "version": "1.4.0",
1474 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1475 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
1476 | "requires": {
1477 | "wrappy": "1"
1478 | }
1479 | },
1480 | "optionator": {
1481 | "version": "0.8.3",
1482 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
1483 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
1484 | "requires": {
1485 | "deep-is": "~0.1.3",
1486 | "fast-levenshtein": "~2.0.6",
1487 | "levn": "~0.3.0",
1488 | "prelude-ls": "~1.1.2",
1489 | "type-check": "~0.3.2",
1490 | "word-wrap": "~1.2.3"
1491 | }
1492 | },
1493 | "pac-proxy-agent": {
1494 | "version": "3.0.1",
1495 | "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz",
1496 | "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==",
1497 | "requires": {
1498 | "agent-base": "^4.2.0",
1499 | "debug": "^4.1.1",
1500 | "get-uri": "^2.0.0",
1501 | "http-proxy-agent": "^2.1.0",
1502 | "https-proxy-agent": "^3.0.0",
1503 | "pac-resolver": "^3.0.0",
1504 | "raw-body": "^2.2.0",
1505 | "socks-proxy-agent": "^4.0.1"
1506 | },
1507 | "dependencies": {
1508 | "debug": {
1509 | "version": "4.1.1",
1510 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
1511 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
1512 | "requires": {
1513 | "ms": "^2.1.1"
1514 | }
1515 | },
1516 | "ms": {
1517 | "version": "2.1.2",
1518 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1519 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1520 | }
1521 | }
1522 | },
1523 | "pac-resolver": {
1524 | "version": "3.0.0",
1525 | "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz",
1526 | "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==",
1527 | "requires": {
1528 | "co": "^4.6.0",
1529 | "degenerator": "^1.0.4",
1530 | "ip": "^1.1.5",
1531 | "netmask": "^1.0.6",
1532 | "thunkify": "^2.1.2"
1533 | }
1534 | },
1535 | "pako": {
1536 | "version": "0.2.9",
1537 | "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
1538 | "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU="
1539 | },
1540 | "parseurl": {
1541 | "version": "1.3.3",
1542 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1543 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
1544 | },
1545 | "path-is-absolute": {
1546 | "version": "1.0.1",
1547 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1548 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
1549 | },
1550 | "path-parse": {
1551 | "version": "1.0.6",
1552 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
1553 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
1554 | },
1555 | "path-to-regexp": {
1556 | "version": "0.1.7",
1557 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1558 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
1559 | },
1560 | "picomatch": {
1561 | "version": "2.2.1",
1562 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
1563 | "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA=="
1564 | },
1565 | "pidusage": {
1566 | "version": "2.0.17",
1567 | "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.17.tgz",
1568 | "integrity": "sha512-N8X5v18rBmlBoArfS83vrnD0gIFyZkXEo7a5pAS2aT0i2OLVymFb2AzVg+v8l/QcXnE1JwZcaXR8daJcoJqtjw==",
1569 | "requires": {
1570 | "safe-buffer": "^5.1.2"
1571 | }
1572 | },
1573 | "pm2": {
1574 | "version": "4.2.3",
1575 | "resolved": "https://registry.npmjs.org/pm2/-/pm2-4.2.3.tgz",
1576 | "integrity": "sha512-aRTl8W6dmZ4S2hti1dX4Xvkpy/yIME1H5pMK0HEOpw1H33j4IAfdzScPoPLYaHeh1oL4biabGwxuyClOM8YUVQ==",
1577 | "requires": {
1578 | "@pm2/agent": "^0.5.26",
1579 | "@pm2/io": "^4.3.2",
1580 | "@pm2/js-api": "^0.5.60",
1581 | "@pm2/pm2-version-check": "^1.0.3",
1582 | "async": "^3.1.0",
1583 | "blessed": "0.1.81",
1584 | "chalk": "2.4.2",
1585 | "chokidar": "^3.2.0",
1586 | "cli-table-redemption": "1.0.1",
1587 | "commander": "2.15.1",
1588 | "cron": "1.7.1",
1589 | "date-fns": "1.30.1",
1590 | "debug": "4.1.1",
1591 | "enquirer": "^2.3.2",
1592 | "eventemitter2": "5.0.1",
1593 | "fclone": "1.0.11",
1594 | "lodash": "4.17.14",
1595 | "mkdirp": "0.5.1",
1596 | "moment": "2.24.0",
1597 | "needle": "2.4.0",
1598 | "pidusage": "2.0.17",
1599 | "pm2-axon": "3.3.0",
1600 | "pm2-axon-rpc": "0.5.1",
1601 | "pm2-deploy": "^0.4.0",
1602 | "pm2-multimeter": "^0.1.2",
1603 | "promptly": "^2",
1604 | "ps-list": "6.3.0",
1605 | "semver": "^5.5",
1606 | "shelljs": "0.8.3",
1607 | "source-map-support": "0.5.12",
1608 | "sprintf-js": "1.1.2",
1609 | "systeminformation": "^4.14.16",
1610 | "vizion": "~2.0.2",
1611 | "yamljs": "0.3.0"
1612 | },
1613 | "dependencies": {
1614 | "debug": {
1615 | "version": "4.1.1",
1616 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
1617 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
1618 | "requires": {
1619 | "ms": "^2.1.1"
1620 | }
1621 | },
1622 | "ms": {
1623 | "version": "2.1.2",
1624 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1625 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1626 | }
1627 | }
1628 | },
1629 | "pm2-axon": {
1630 | "version": "3.3.0",
1631 | "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-3.3.0.tgz",
1632 | "integrity": "sha512-dAFlFYRuFbFjX7oAk41zT+dx86EuaFX/TgOp5QpUKRKwxb946IM6ydnoH5sSTkdI2pHSVZ+3Am8n/l0ocr7jdQ==",
1633 | "requires": {
1634 | "amp": "~0.3.1",
1635 | "amp-message": "~0.1.1",
1636 | "debug": "^3.0",
1637 | "escape-regexp": "0.0.1"
1638 | },
1639 | "dependencies": {
1640 | "debug": {
1641 | "version": "3.2.6",
1642 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
1643 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
1644 | "requires": {
1645 | "ms": "^2.1.1"
1646 | }
1647 | },
1648 | "ms": {
1649 | "version": "2.1.2",
1650 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1651 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1652 | }
1653 | }
1654 | },
1655 | "pm2-axon-rpc": {
1656 | "version": "0.5.1",
1657 | "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.5.1.tgz",
1658 | "integrity": "sha512-hT8gN3/j05895QLXpwg+Ws8PjO4AVID6Uf9StWpud9HB2homjc1KKCcI0vg9BNOt56FmrqKDT1NQgheIz35+sA==",
1659 | "requires": {
1660 | "debug": "^3.0"
1661 | },
1662 | "dependencies": {
1663 | "debug": {
1664 | "version": "3.2.6",
1665 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
1666 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
1667 | "requires": {
1668 | "ms": "^2.1.1"
1669 | }
1670 | },
1671 | "ms": {
1672 | "version": "2.1.2",
1673 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1674 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1675 | }
1676 | }
1677 | },
1678 | "pm2-deploy": {
1679 | "version": "0.4.0",
1680 | "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-0.4.0.tgz",
1681 | "integrity": "sha512-3BdCghcGwMKwl3ffHZhc+j5JY5dldH9nq8m/I9W5wehJuSRZIyO96VOgKTMv3hYp7Yk5E+2lRGm8WFNlp65vOA==",
1682 | "requires": {
1683 | "async": "^2.6",
1684 | "tv4": "^1.3"
1685 | },
1686 | "dependencies": {
1687 | "async": {
1688 | "version": "2.6.3",
1689 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
1690 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
1691 | "requires": {
1692 | "lodash": "^4.17.14"
1693 | }
1694 | }
1695 | }
1696 | },
1697 | "pm2-multimeter": {
1698 | "version": "0.1.2",
1699 | "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz",
1700 | "integrity": "sha1-Gh5VFT1BoFU0zqI8/oYKuqDrSs4=",
1701 | "requires": {
1702 | "charm": "~0.1.1"
1703 | }
1704 | },
1705 | "prelude-ls": {
1706 | "version": "1.1.2",
1707 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
1708 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
1709 | },
1710 | "process-nextick-args": {
1711 | "version": "2.0.1",
1712 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
1713 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
1714 | },
1715 | "promptly": {
1716 | "version": "2.2.0",
1717 | "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz",
1718 | "integrity": "sha1-KhP6BjaIoqWYOxYf/wEIoH0m/HQ=",
1719 | "requires": {
1720 | "read": "^1.0.4"
1721 | }
1722 | },
1723 | "proxy-addr": {
1724 | "version": "2.0.5",
1725 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
1726 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
1727 | "requires": {
1728 | "forwarded": "~0.1.2",
1729 | "ipaddr.js": "1.9.0"
1730 | }
1731 | },
1732 | "proxy-agent": {
1733 | "version": "3.1.1",
1734 | "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz",
1735 | "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==",
1736 | "requires": {
1737 | "agent-base": "^4.2.0",
1738 | "debug": "4",
1739 | "http-proxy-agent": "^2.1.0",
1740 | "https-proxy-agent": "^3.0.0",
1741 | "lru-cache": "^5.1.1",
1742 | "pac-proxy-agent": "^3.0.1",
1743 | "proxy-from-env": "^1.0.0",
1744 | "socks-proxy-agent": "^4.0.1"
1745 | },
1746 | "dependencies": {
1747 | "debug": {
1748 | "version": "4.1.1",
1749 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
1750 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
1751 | "requires": {
1752 | "ms": "^2.1.1"
1753 | }
1754 | },
1755 | "ms": {
1756 | "version": "2.1.2",
1757 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1758 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1759 | }
1760 | }
1761 | },
1762 | "proxy-from-env": {
1763 | "version": "1.0.0",
1764 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
1765 | "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
1766 | },
1767 | "ps-list": {
1768 | "version": "6.3.0",
1769 | "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz",
1770 | "integrity": "sha512-qau0czUSB0fzSlBOQt0bo+I2v6R+xiQdj78e1BR/Qjfl5OHWJ/urXi8+ilw1eHe+5hSeDI1wrwVTgDp2wst4oA=="
1771 | },
1772 | "qs": {
1773 | "version": "6.7.0",
1774 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
1775 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
1776 | },
1777 | "range-parser": {
1778 | "version": "1.2.1",
1779 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1780 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
1781 | },
1782 | "raw-body": {
1783 | "version": "2.4.0",
1784 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
1785 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
1786 | "requires": {
1787 | "bytes": "3.1.0",
1788 | "http-errors": "1.7.2",
1789 | "iconv-lite": "0.4.24",
1790 | "unpipe": "1.0.0"
1791 | }
1792 | },
1793 | "read": {
1794 | "version": "1.0.7",
1795 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
1796 | "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
1797 | "requires": {
1798 | "mute-stream": "~0.0.4"
1799 | }
1800 | },
1801 | "readable-stream": {
1802 | "version": "2.3.7",
1803 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
1804 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
1805 | "requires": {
1806 | "core-util-is": "~1.0.0",
1807 | "inherits": "~2.0.3",
1808 | "isarray": "~1.0.0",
1809 | "process-nextick-args": "~2.0.0",
1810 | "safe-buffer": "~5.1.1",
1811 | "string_decoder": "~1.1.1",
1812 | "util-deprecate": "~1.0.1"
1813 | },
1814 | "dependencies": {
1815 | "isarray": {
1816 | "version": "1.0.0",
1817 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
1818 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
1819 | },
1820 | "string_decoder": {
1821 | "version": "1.1.1",
1822 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
1823 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
1824 | "requires": {
1825 | "safe-buffer": "~5.1.0"
1826 | }
1827 | }
1828 | }
1829 | },
1830 | "readdirp": {
1831 | "version": "3.3.0",
1832 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
1833 | "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
1834 | "requires": {
1835 | "picomatch": "^2.0.7"
1836 | }
1837 | },
1838 | "rechoir": {
1839 | "version": "0.6.2",
1840 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
1841 | "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
1842 | "requires": {
1843 | "resolve": "^1.1.6"
1844 | }
1845 | },
1846 | "referrer-policy": {
1847 | "version": "1.2.0",
1848 | "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
1849 | "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA=="
1850 | },
1851 | "require-in-the-middle": {
1852 | "version": "5.0.3",
1853 | "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.0.3.tgz",
1854 | "integrity": "sha512-p/ICV8uMlqC4tjOYabLMxAWCIKa0YUQgZZ6KDM0xgXJNgdGQ1WmL2A07TwmrZw+wi6ITUFKzH5v3n+ENEyXVkA==",
1855 | "requires": {
1856 | "debug": "^4.1.1",
1857 | "module-details-from-path": "^1.0.3",
1858 | "resolve": "^1.12.0"
1859 | },
1860 | "dependencies": {
1861 | "debug": {
1862 | "version": "4.1.1",
1863 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
1864 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
1865 | "requires": {
1866 | "ms": "^2.1.1"
1867 | }
1868 | },
1869 | "ms": {
1870 | "version": "2.1.2",
1871 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1872 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1873 | }
1874 | }
1875 | },
1876 | "resolve": {
1877 | "version": "1.15.1",
1878 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
1879 | "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
1880 | "requires": {
1881 | "path-parse": "^1.0.6"
1882 | }
1883 | },
1884 | "safe-buffer": {
1885 | "version": "5.1.2",
1886 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
1887 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
1888 | },
1889 | "safer-buffer": {
1890 | "version": "2.1.2",
1891 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1892 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1893 | },
1894 | "sax": {
1895 | "version": "1.2.4",
1896 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
1897 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
1898 | },
1899 | "semver": {
1900 | "version": "5.7.1",
1901 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
1902 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
1903 | },
1904 | "send": {
1905 | "version": "0.17.1",
1906 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
1907 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
1908 | "requires": {
1909 | "debug": "2.6.9",
1910 | "depd": "~1.1.2",
1911 | "destroy": "~1.0.4",
1912 | "encodeurl": "~1.0.2",
1913 | "escape-html": "~1.0.3",
1914 | "etag": "~1.8.1",
1915 | "fresh": "0.5.2",
1916 | "http-errors": "~1.7.2",
1917 | "mime": "1.6.0",
1918 | "ms": "2.1.1",
1919 | "on-finished": "~2.3.0",
1920 | "range-parser": "~1.2.1",
1921 | "statuses": "~1.5.0"
1922 | },
1923 | "dependencies": {
1924 | "ms": {
1925 | "version": "2.1.1",
1926 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
1927 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
1928 | }
1929 | }
1930 | },
1931 | "serve-static": {
1932 | "version": "1.14.1",
1933 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
1934 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
1935 | "requires": {
1936 | "encodeurl": "~1.0.2",
1937 | "escape-html": "~1.0.3",
1938 | "parseurl": "~1.3.3",
1939 | "send": "0.17.1"
1940 | }
1941 | },
1942 | "setprototypeof": {
1943 | "version": "1.1.1",
1944 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
1945 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
1946 | },
1947 | "shelljs": {
1948 | "version": "0.8.3",
1949 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
1950 | "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
1951 | "requires": {
1952 | "glob": "^7.0.0",
1953 | "interpret": "^1.0.0",
1954 | "rechoir": "^0.6.2"
1955 | }
1956 | },
1957 | "shimmer": {
1958 | "version": "1.2.1",
1959 | "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
1960 | "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
1961 | },
1962 | "signal-exit": {
1963 | "version": "3.0.2",
1964 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
1965 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
1966 | },
1967 | "smart-buffer": {
1968 | "version": "4.1.0",
1969 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz",
1970 | "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw=="
1971 | },
1972 | "socks": {
1973 | "version": "2.3.3",
1974 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz",
1975 | "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==",
1976 | "requires": {
1977 | "ip": "1.1.5",
1978 | "smart-buffer": "^4.1.0"
1979 | }
1980 | },
1981 | "socks-proxy-agent": {
1982 | "version": "4.0.2",
1983 | "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz",
1984 | "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==",
1985 | "requires": {
1986 | "agent-base": "~4.2.1",
1987 | "socks": "~2.3.2"
1988 | },
1989 | "dependencies": {
1990 | "agent-base": {
1991 | "version": "4.2.1",
1992 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
1993 | "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
1994 | "requires": {
1995 | "es6-promisify": "^5.0.0"
1996 | }
1997 | }
1998 | }
1999 | },
2000 | "source-map": {
2001 | "version": "0.6.1",
2002 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
2003 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
2004 | },
2005 | "source-map-support": {
2006 | "version": "0.5.12",
2007 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
2008 | "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
2009 | "requires": {
2010 | "buffer-from": "^1.0.0",
2011 | "source-map": "^0.6.0"
2012 | }
2013 | },
2014 | "sprintf-js": {
2015 | "version": "1.1.2",
2016 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
2017 | "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
2018 | },
2019 | "statuses": {
2020 | "version": "1.5.0",
2021 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
2022 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
2023 | },
2024 | "string_decoder": {
2025 | "version": "0.10.31",
2026 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
2027 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
2028 | },
2029 | "strip-ansi": {
2030 | "version": "3.0.1",
2031 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
2032 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
2033 | "requires": {
2034 | "ansi-regex": "^2.0.0"
2035 | }
2036 | },
2037 | "supports-color": {
2038 | "version": "5.5.0",
2039 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
2040 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
2041 | "requires": {
2042 | "has-flag": "^3.0.0"
2043 | }
2044 | },
2045 | "systeminformation": {
2046 | "version": "4.21.2",
2047 | "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.21.2.tgz",
2048 | "integrity": "sha512-7O6laxHTstfj9MSrX77lKLfWz0zqqutWL0+uoJkR7sOOr4XCTwD0QG4shUVCiOWlVX+a8/gHJUio420FrSk8/w==",
2049 | "optional": true
2050 | },
2051 | "thunkify": {
2052 | "version": "2.1.2",
2053 | "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz",
2054 | "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0="
2055 | },
2056 | "to-regex-range": {
2057 | "version": "5.0.1",
2058 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2059 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2060 | "requires": {
2061 | "is-number": "^7.0.0"
2062 | }
2063 | },
2064 | "toidentifier": {
2065 | "version": "1.0.0",
2066 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
2067 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
2068 | },
2069 | "tslib": {
2070 | "version": "1.9.3",
2071 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
2072 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
2073 | },
2074 | "tv4": {
2075 | "version": "1.3.0",
2076 | "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
2077 | "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
2078 | },
2079 | "type-check": {
2080 | "version": "0.3.2",
2081 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
2082 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
2083 | "requires": {
2084 | "prelude-ls": "~1.1.2"
2085 | }
2086 | },
2087 | "type-is": {
2088 | "version": "1.6.18",
2089 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
2090 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
2091 | "requires": {
2092 | "media-typer": "0.3.0",
2093 | "mime-types": "~2.1.24"
2094 | }
2095 | },
2096 | "ultron": {
2097 | "version": "1.1.1",
2098 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
2099 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
2100 | },
2101 | "unpipe": {
2102 | "version": "1.0.0",
2103 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2104 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
2105 | },
2106 | "util-deprecate": {
2107 | "version": "1.0.2",
2108 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2109 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
2110 | },
2111 | "utils-merge": {
2112 | "version": "1.0.1",
2113 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2114 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
2115 | },
2116 | "uuid": {
2117 | "version": "3.4.0",
2118 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
2119 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
2120 | },
2121 | "vary": {
2122 | "version": "1.1.2",
2123 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2124 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
2125 | },
2126 | "vizion": {
2127 | "version": "2.0.2",
2128 | "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.0.2.tgz",
2129 | "integrity": "sha512-UGDB/UdC1iyPkwyQaI9AFMwKcluQyD4FleEXObrlu254MEf16MV8l+AZdpFErY/iVKZVWlQ+OgJlVVJIdeMUYg==",
2130 | "requires": {
2131 | "async": "2.6.1",
2132 | "git-node-fs": "^1.0.0",
2133 | "ini": "^1.3.4",
2134 | "js-git": "^0.7.8",
2135 | "lodash.findindex": "^4.6.0",
2136 | "lodash.foreach": "^4.5.0",
2137 | "lodash.get": "^4.4.2",
2138 | "lodash.last": "^3.0.0"
2139 | },
2140 | "dependencies": {
2141 | "async": {
2142 | "version": "2.6.1",
2143 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
2144 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
2145 | "requires": {
2146 | "lodash": "^4.17.10"
2147 | }
2148 | }
2149 | }
2150 | },
2151 | "word-wrap": {
2152 | "version": "1.2.3",
2153 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
2154 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
2155 | },
2156 | "wrappy": {
2157 | "version": "1.0.2",
2158 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2159 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
2160 | },
2161 | "ws": {
2162 | "version": "5.2.2",
2163 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
2164 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
2165 | "requires": {
2166 | "async-limiter": "~1.0.0"
2167 | }
2168 | },
2169 | "x-xss-protection": {
2170 | "version": "1.3.0",
2171 | "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz",
2172 | "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg=="
2173 | },
2174 | "xregexp": {
2175 | "version": "2.0.0",
2176 | "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
2177 | "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM="
2178 | },
2179 | "yallist": {
2180 | "version": "3.1.1",
2181 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2182 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
2183 | },
2184 | "yamljs": {
2185 | "version": "0.3.0",
2186 | "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz",
2187 | "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==",
2188 | "requires": {
2189 | "argparse": "^1.0.7",
2190 | "glob": "^7.0.5"
2191 | }
2192 | }
2193 | }
2194 | }
2195 |
--------------------------------------------------------------------------------