├── 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 |
8 | ADD CONTACT 9 |
10 | <%- include('../common/add-fields', {}); %> 11 | 12 |
13 |
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 |
2 | SIGN UP 3 |
4 | <%- include('../common/add-fields', {}); %> 5 |
6 | 7 | 14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /client/views/contacts/list.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% for(const ct of contacts){ %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% } %> 19 |
First NameLast NamePhoneEmail
<%= ct.firstName %><%= ct.lastName %><%= ct.primaryContactNumber %><%= ct.primaryEmailAddress %>
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 |
2 | SIGN IN 3 |
4 |
5 | 6 | 14 |
15 |
16 | 17 | 24 |
25 | 26 | 27 |
28 |
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 | ` 27 | 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 | 51 | 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 | --------------------------------------------------------------------------------
Contact IDContact Data
${_id}${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")}