From 06310a54810d779a323bb6cf1fa7b4769ae92700 Mon Sep 17 00:00:00 2001 From: ebelcrom Date: Sun, 3 Nov 2019 00:46:18 +0100 Subject: [PATCH] sources added --- .gitignore | 61 +++ app.js | 72 +++ bin/www | 90 ++++ init/initdb.js | 61 +++ lib/dblib.js | 145 ++++++ lib/logger.js | 67 +++ package-lock.json | 933 +++++++++++++++++++++++++++++++++++ package.json | 24 + public/images/closed.jpg | Bin 0 -> 9756 bytes public/images/moving.jpg | Bin 0 -> 9154 bytes public/images/open.jpg | Bin 0 -> 8498 bytes public/index.html | 13 + public/stylesheets/style.css | 8 + routes/index.js | 9 + routes/v1/control.js | 9 + routes/v1/events.js | 9 + routes/v1/status.js | 30 ++ routes/v1/test/control.js | 87 ++++ routes/v1/test/events.js | 118 +++++ routes/v1/test/settings.js | 136 +++++ routes/v1/test/status.js | 78 +++ spec/garnod.json | 474 ++++++++++++++++++ spec/garnod.yaml | 323 ++++++++++++ 23 files changed, 2747 insertions(+) create mode 100644 .gitignore create mode 100644 app.js create mode 100755 bin/www create mode 100644 init/initdb.js create mode 100644 lib/dblib.js create mode 100644 lib/logger.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/images/closed.jpg create mode 100644 public/images/moving.jpg create mode 100644 public/images/open.jpg create mode 100644 public/index.html create mode 100644 public/stylesheets/style.css create mode 100644 routes/index.js create mode 100644 routes/v1/control.js create mode 100644 routes/v1/events.js create mode 100644 routes/v1/status.js create mode 100644 routes/v1/test/control.js create mode 100644 routes/v1/test/events.js create mode 100644 routes/v1/test/settings.js create mode 100644 routes/v1/test/status.js create mode 100644 spec/garnod.json create mode 100644 spec/garnod.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1bed12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/app.js b/app.js new file mode 100644 index 0000000..23ac4a9 --- /dev/null +++ b/app.js @@ -0,0 +1,72 @@ +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const logger = require('morgan'); + +const log = require('./lib/logger')(__filename.slice(__dirname.length + 1)); +const ver = 'v1'; + +const indexRouter = require('./routes/index'); + +// routes +const prodStatus = require('./routes/' + ver + '/status'); +const prodEvents = require('./routes/' + ver + '/events'); +const prodControl = require('./routes/' + ver + '/control'); +const testStatus = require('./routes/' + ver + '/test/status'); +const testEvents = require('./routes/' + ver + '/test/events').router; +const testControl = require('./routes/' + ver + '/test/control'); +const testSettings = require('./routes/' + ver + '/test/settings'); + +// api-docs +const swaggerUi = require('swagger-ui-express'); +const swaggerDocument = require('./spec/garnod.json'); + +const app = express(); + +app.use(logger('dev')); +app.use(express.json({ + verify: (req, res, buf, enc) => { + try { + JSON.parse(buf); + } catch (e) { + res.status(400).send(); + throw Error('Invalid JSON'); + } + } +})); +app.use((err, req, res, next) => { + if (err instanceof Error) { + log.warn(JSON.stringify(err)); + } +}); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); + +app.use('/' + ver + '/spec', express.static('spec')); + +// routes +app.use('/' + ver + '/status', prodStatus); +app.use('/' + ver + '/events', prodEvents); +app.use('/' + ver + '/control', prodControl); +app.use('/' + ver + '/test/status', testStatus); +app.use('/' + ver + '/test/events', testEvents); +app.use('/' + ver + '/test/control', testControl); +app.use('/' + ver + '/test/settings', testSettings); + +// api-docs +app.use('/' + ver + '/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +/* +app.use((err, req, res, next) => { +log.error(err); + res.status(err.status).json({ + message: err.message, + errors: err.errors, + }); +}); +*/ + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..6ed2878 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('garnod:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/init/initdb.js b/init/initdb.js new file mode 100644 index 0000000..d95b06c --- /dev/null +++ b/init/initdb.js @@ -0,0 +1,61 @@ +const mongo = require('mongodb').MongoClient; +const url = "mongodb://localhost:27017"; +const name = 'garnod'; + +// create db +mongo.connect(url + '/' + name, function(err, db) { + if (err) { + throw err; + return; + } + console.log('garnod created!'); + + // create collections + var dbo = db.db(name); + dbo.createCollection('keys', function(err, res) { + if (err) { + throw err; + return; + } + console.log('keys created!'); + + // populate collection + dbo.collection('keys').insertOne({ + _id: 1, + area: 'test', + key: '2TTqCD4mNNny' + }, function(err, res) { + if (err) { + throw err; + return; + } + console.log('test key set!'); + }); + }); + + dbo.createCollection('settings', function(err, res) { + if (err) { + throw err; + return; + } + console.log('settings created!'); + + // populate collection + dbo.collection('settings').insertOne({ + _id: 1, + status: { + state: 'closed' + }, + events: { + state: 'closed', + delay: 0 + } + }, function(err, res) { + if (err) { + throw err; + return; + } + console.log('settings set!'); + }); + }); +}); diff --git a/lib/dblib.js b/lib/dblib.js new file mode 100644 index 0000000..a0cc9bd --- /dev/null +++ b/lib/dblib.js @@ -0,0 +1,145 @@ +const MongoClient = require('mongodb').MongoClient; +const log = require('./logger')(__filename.slice(__dirname.length + 1)); +const url = 'mongodb://localhost:27017'; +const name = 'garnod'; +var db = null; + +const msg = { + ok: 'ok', + dbError: 'dbError', + keyMismatch: 'keyMismatch' +}; + +function connect() { + // connect to mongodb/mydb once, auto_reconnect is set by default + if (db === null) { + MongoClient.connect(url, function(err, connection) { + if (err) { + log.error('Connection to database failed', JSON.stringify(err)); + } else { + log.info('Connection to database opened'); + db = connection; + // handle irreversible connection lost + db.on('close', function() { + log.error('Connection to DB permanently lost'); + process.exit(1); + }); + } + }); + } +} + +// connect to DB immediately +connect(); + +function setSettings(data, callback) { + // resolve API key + new Promise((resolve, reject) => { + var keys = db.db(name).collection('keys'); + log.debug('Search for API key, area', data.area); + keys.findOne({ area: data.area }, {}, (err, res) => { + if (err) { + log.error('Error on searching for API key', JSON.stringify(err)); + return reject(msg.dbError); + } + if (res === null) { + log.warn('No such key area found in DB'); + return reject(msg.dbError); + } + if (res.key !== data.key) { + log.info('API key doesn\'t match'); + return reject(msg.keyMismatch); + } + log.debug('API key is valid'); + resolve(msg.ok); + }) + }) + .catch (err => { + log.error('Error on searching for API key', err); + return callback(err, null); + }) + + // save settings + .then(res => { + return new Promise((resolve, reject) => { + var settings = db.db(name).collection('settings'); + log.debug('Update settings:', JSON.stringify(data.settings)); + settings.updateOne({ _id: 1 }, { $set: data.settings }, { upsert: true }, (err, res) => { + if (err) { + log.error('Error on updating settings', JSON.stringify(err)); + return reject(msg.dbError); + } + log.debug('Update settings done'); + resolve(msg.ok); + }); + }); + }) + .catch (err => { + log.error('Error on updating settings', err); + return callback(err, null); + }) + + // return result + .then(res => { + return callback(null, res); + }); +} + +function getSettings(data, callback) { + // resolve API key + new Promise((resolve, reject) => { + var keys = db.db(name).collection('keys'); + log.debug('Search for API key, area', data.area); + keys.findOne({ area: data.area }, {}, (err, res) => { + if (err) { + log.error('Error on searching for API key', JSON.stringify(err)); + return reject(msg.dbError); + } + if (res === null) { + log.warn('No such key area found in DB'); + return reject(msg.dbError); + } + if (res.key !== data.key) { + log.info('API key doesn\'t match'); + return reject(msg.keyMismatch); + } + log.debug('API key is valid'); + resolve(msg.ok); + }) + }) + .catch (err => { + log.error('Error on searching for API key', err); + return callback(err, null); + }) + + // get settings + .then(res => { + return new Promise((resolve, reject) => { + var settings = db.db(name).collection('settings'); + log.debug('Search for settings object'); + settings.findOne({ _id: 1 }, {}, (err, res) => { + if (err) { + log.error('Error on getting settings', JSON.stringify(err)); + return reject(msg.dbError); + } + log.debug('Get settings done:', JSON.stringify(res)); + resolve(res); + }); + }); + }) + .catch (err => { + log.error('Error on getting settings', err); + return callback(err, null); + }) + + // return result + .then(res => { + return callback(null, res); + }); +} + +module.exports = { + msg, + setSettings, + getSettings +} diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..c8b145b --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,67 @@ +const winston = require("winston"); +const dateformat = require("dateformat"); + +const custom = { + levels: { + error: 5, + warn: 4, + info: 3, + todo: 2, + verbose: 1, + debug: 0 + }, + colors: { + error: 'red', + warn: 'yellow', + info: 'green', + todo: 'magenta', + verbose: 'blue', + debug: 'gray' + } +}; + +var level = "error"; +if (typeof process.env.LOGLEVEL != "undefined") { + level = process.env.LOGLEVEL; +}; + +winston.addColors(custom.colors); + +const logger = new (winston.Logger) ({ + levels: custom.levels, + transports: [ + new (winston.transports.Console) ({ + level: level, +// timestamp() { +// var now = new Date(); +// return dateformat(now, "HH:MM:ss.l"); +// }, + colorize: true + }) + ] +}); + +module.exports = function(fileName) { + var myLogger = { + error: function(...args) { + logger.error('[' + fileName + ']', ...args); + }, + warn: function(...args) { + logger.warn('[' + fileName + ']', ...args); + }, + info: function(...args) { + logger.info('[' + fileName + ']', ...args); + }, + todo: function(...args) { + logger.todo('[' + fileName + ']', ...args); + }, + verbose: function(...args) { + logger.verbose('[' + fileName + ']', ...args); + }, + debug: function(...args) { + logger.debug('[' + fileName + ']', ...args); + }, + } + + return myLogger +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..713a7ba --- /dev/null +++ b/package-lock.json @@ -0,0 +1,933 @@ +{ + "name": "garnod", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "bson": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "dateformat": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.11.tgz", + "integrity": "sha1-8ny+56ASu/uC6gUVYtOXf2CT27E=", + "requires": { + "get-stdin": "*", + "meow": "*" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-generator": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/express-generator/-/express-generator-4.16.1.tgz", + "integrity": "sha512-tWYEx5Y/Llos2qC6yAETmdqEMEPqNUzJ8btGcSZ2zSr8RYOalzffhvh9zx5OQTctvOgJ9kKYxyvFGAIuUuF/wA==", + "dev": true, + "requires": { + "commander": "2.15.1", + "ejs": "2.6.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "sorted-object": "2.0.1" + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" + }, + "hosted-git-info": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", + "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mongodb": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.2.tgz", + "integrity": "sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg==", + "requires": { + "bson": "^1.1.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2" + } + }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.8.3.tgz", + "integrity": "sha512-llcxWccnyaWlODe7A9hRjkvdCKamEKTh+wH8ITdTc3OhchaqUZteiSCX/2ablWHVrkVIe04dntnaZJ7BdyW0lQ==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sorted-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz", + "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" + }, + "swagger-ui-dist": { + "version": "3.23.11", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.11.tgz", + "integrity": "sha512-ipENHHH/sqpngTpHXUwg55eAOZ7b2UVayUwwuWPA6nQSPhjBVXX4zOPpNKUwQIFOl3oIwVvZF7mqoxH7pMgVzA==" + }, + "swagger-ui-express": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz", + "integrity": "sha512-bVT16qj6WdNlEKFkSLOoTeGuqEm2lfOFRq6mVHAx+viA/ikORE+n4CS3WpVcYmQzM4HE6+DUFgAWcMRBJNpjcw==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "winston": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-1.1.2.tgz", + "integrity": "sha1-aO3Xaf951PlSjPDl2AAhqt5nSAw=", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "pkginfo": "0.3.x", + "stack-trace": "0.0.x" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "requires": { + "camelcase": "^4.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90e6ebb --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "garnod", + "version": "0.0.0", + "description": "A garage door monitoring application.", + "scripts": { + "start": "node ./bin/www" + }, + "author": "ebelcrom", + "license": "GPL-2.0-or-later", + "dependencies": { + "cookie-parser": "~1.4.4", + "dateformat": "1.0.11", + "debug": "~2.6.9", + "express": "~4.16.1", + "mongodb": "^3.3.2", + "morgan": "~1.9.1", + "query-string": "^6.8.3", + "swagger-ui-express": "^4.1.2", + "winston": "1.1.2" + }, + "devDependencies": { + "express-generator": "^4.16.1" + } +} diff --git a/public/images/closed.jpg b/public/images/closed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9de6f5fc9bf2e7011241afccc75c930c5c533a6a GIT binary patch literal 9756 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 literal 0 HcmV?d00001 diff --git a/public/images/moving.jpg b/public/images/moving.jpg new file mode 100644 index 0000000000000000000000000000000000000000..279f614981c7163369bb1d94b686b0ecf9ac8b83 GIT binary patch literal 9154 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 b9nA>NOT-}ihWmM=I@m%(*Jfz*FUJ25&r)(b literal 0 HcmV?d00001 diff --git a/public/images/open.jpg b/public/images/open.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be1632b81d21593285dd68f9173052d1c667b79a GIT binary patch literal 8498 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 literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ab1ad8a --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + Express + + + + +

Express

+

Welcome to Express

+ + + diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/routes/index.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/routes/v1/control.js b/routes/v1/control.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/routes/v1/control.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/routes/v1/events.js b/routes/v1/events.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/routes/v1/events.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/routes/v1/status.js b/routes/v1/status.js new file mode 100644 index 0000000..23e5332 --- /dev/null +++ b/routes/v1/status.js @@ -0,0 +1,30 @@ +var express = require('express'); +const log = require('./../../lib/logger')(__filename.slice(__dirname.length + 1)); +const qStr = require('query-string'); +var router = express.Router(); + +/* GET /v1/status?image=true */ +router.get('/', function(req, res, next) { + if (typeof req.query.image == 'string') { + switch (req.query.image) { + case 'true': + res.json({ + 'image': 'image', + 'state': 'open' + }); + break; + case 'false': + res.json({ + 'state': 'open' + }); + break; + default: + res.status(400).json([ 'image' ]); + break; + } + } else { + res.status(400).json([ 'image' ]); + } +}); + +module.exports = router; diff --git a/routes/v1/test/control.js b/routes/v1/test/control.js new file mode 100644 index 0000000..21821cc --- /dev/null +++ b/routes/v1/test/control.js @@ -0,0 +1,87 @@ +const express = require('express'); +const log = require('./../../../lib/logger')(__filename.slice(__dirname.length + 1)); +const qStr = require('query-string'); +const router = express.Router(); +const dblib = require('./../../../lib/dblib'); +const ev = require('./events'); + +const events = ev.events; +var timer = null; + +/* POST /vN/test/control */ +router.post('/', function(req, res, next) { + // check header + if (typeof req.header('X-API-Key-Test') === 'undefined') { + log.info('API key not set in request header'); + res.status(401).send(); + return; + } else { + log.debug('API key set in request header'); + } + + // check query parameter + var command = 'move'; + if (typeof req.query.command != 'undefined') { + if (typeof req.query.command == 'string') { + switch (req.query.command) { + case 'move': + move(req, res, next); + break; + default: + log.info('Value of command query parameter unknown'); + res.status(400).send(); + return; + } + } + } else { + // default action + move(req, res, next); + } +}); + +function move(req, res, next) { + // get settings + dblib.getSettings({ + area: 'test', + key: req.header('X-API-Key-Test') + }, (err, data) => { + if (err) { + switch (err) { + case dblib.msg.dbError: + log.info('Server error response'); + res.status(500).send(); + break; + case dblib.msg.keyMismatch: + log.info('Unauthorized access'); + res.status(401).send(); + break; + default: + log.error('Error result unexpected'); + res.status(500).send(); + break; + } + } else { + // schedule event + scheduleEvent(data.events.delay, data); + // send response + res.status(200).send(); + } + }); +} + +function scheduleEvent(delay, settings) { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + timer = setTimeout(executeEvent, delay * 1000, settings); + log.debug('Event set to execute in ' + delay + ' seconds'); +} + +function executeEvent(settings) { + clearTimeout(timer); + timer = null; + events.emit('event', settings); +} + +module.exports = router; diff --git a/routes/v1/test/events.js b/routes/v1/test/events.js new file mode 100644 index 0000000..f39d2ff --- /dev/null +++ b/routes/v1/test/events.js @@ -0,0 +1,118 @@ +const express = require('express'); +const log = require('./../../../lib/logger')(__filename.slice(__dirname.length + 1)); +const qStr = require('query-string'); +const router = express.Router(); +const dblib = require('./../../../lib/dblib'); +const EventEmitter = require('events'); +const fs = require('fs'); + +const events = new EventEmitter(); +var response = null; +var request = null; +var timer = null; +const delayMin = 1; +const delayMax = 300; + +/* GET /vN/test/events */ +router.get('/', function(req, res, next) { + // check header + if (typeof req.header('X-API-Key-Test') === 'undefined') { + log.info('API key not set in request header'); + res.status(401).send(); + return; + } else { + log.debug('API key set in request header'); + } + + // TODO: dequeue? + + // check query parameter + var timeout = 30; + if (typeof req.query.timeout != 'undefined') { + var delay = parseInt(req.query.timeout); + if (!isNaN(delay)) { + if (delay < delayMin || delay > delayMax) { + log.info('Value of timeout query parameter not in range'); + res.status(400).send(); + return; + } else { + timeout = delay; + } + } + } + + // schedule resonse on timeout + response = res; + request = req; + if (timer === null) { + timer = setTimeout(processTimeout, timeout * 1000); + log.debug('Response timeout scheduled'); + } else { + log.todo('not implemented'); + } +}); + +events.on('event', (settings) => { + log.debug('Event ready for sending'); + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + + // read settings + log.debug('Got settings:', JSON.stringify(settings)); + var image = true; + if (request !== null) { + if (typeof request.query.image != 'undefined') { + if (request.query.image === 'false') { + image = false; + } + } + request = null; + } + + // TODO: enqueue? + + // response + if (response !== null) { + var content = null; + if (image) { + var file = null; + switch (settings.events.state) { + case 'open': + file = fs.readFileSync(__dirname + '/../../../public/images/open.jpg', 'base64'); + break; + case 'closed': + file = fs.readFileSync(__dirname + '/../../../public/images/closed.jpg', 'base64'); + break; + default: + log.error('Unexpected status from settings'); + response.status(500).send(); + return; + } + content = { + 'image': file, + 'state': settings.events.state + }; + } else { + content = { + 'state': settings.events.state + }; + } + response.json(content); + response = null; + } +}); + +function processTimeout() { + log.debug('Timeout, no events'); + clearTimeout(timer); + timer = null; + response.status(304).send(); + response = null; +} + +module.exports = { + router, + events +} diff --git a/routes/v1/test/settings.js b/routes/v1/test/settings.js new file mode 100644 index 0000000..07e3910 --- /dev/null +++ b/routes/v1/test/settings.js @@ -0,0 +1,136 @@ +const express = require('express'); +const log = require('./../../../lib/logger')(__filename.slice(__dirname.length + 1)); +const qStr = require('query-string'); +const router = express.Router(); +const dblib = require('./../../../lib/dblib'); + +const delayMin = 1; +const delayMax = 300; + +/* POST /vN/test/settings */ +router.post('/', function(req, res, next) { + // check header + if (typeof req.header('X-API-Key-Test') === 'undefined') { + log.info('API key not set in request header'); + res.status(401).send(); + return; + } else { + log.debug('API key set in request header'); + } + + // check body + if (typeof req.body != 'object') { + log.info('Request body is not JSON'); + res.status(400).send(); + return; + } else{ + var settings = {}; + + // populate status + if (typeof req.body.status == 'object' ) { + if (typeof req.body.status.state == 'string' ) { + switch (req.body.status.state) { + case 'open': + settings.status = { + state: 'open' + }; + break; + case 'closed': + settings.status = { + state: 'closed' + }; + break; + case 'moving': + settings.status = { + state: 'moving' + }; + break; + default: + log.info('Value of status.state in content unknown'); + res.status(400).send(); + return; + } + } else { + log.info('Type of status.state in content invalid'); + res.status(400).send(); + return; + } + } + + // populate events + if (typeof req.body.events == 'object' ) { + if (typeof req.body.events.state == 'string' ) { + switch (req.body.events.state) { + case 'open': + settings.events = { + state: 'open' + }; + break; + case 'closed': + settings.events = { + state: 'closed' + }; + break; + default: + log.info('Value of events.state in content unknown'); + res.status(400).send(); + return; + } + } else { + log.info('Type of events.state in content invalid'); + res.status(400).send(); + return; + } + if (typeof req.body.events.delay == 'number' ) { + var delay = parseInt(req.body.events.delay); + if (delay < delayMin || delay > delayMax) { + log.info('Value of events.delay in content not in range'); + res.status(400).send(); + return; + } else { + settings.events.delay = delay; + } + } else { + log.info('Type of events.delay in content invalid'); + res.status(400).send(); + return; + } + } + log.debug('Parsed settings:', JSON.stringify(settings)); + + // check settings + if (Object.getOwnPropertyNames(settings).length === 0) { + log.info('Settings is empty'); + res.status(400).send(); + return; + } + + // save to DB + dblib.setSettings({ + area: 'test', + key: req.header('X-API-Key-Test'), + settings: settings + }, (err, data) => { + if (err) { + switch (err) { + case dblib.msg.dbError: + log.info('Server error response'); + res.status(500).send(); + break; + case dblib.msg.keyMismatch: + log.info('Unauthorized access'); + res.status(401).send(); + break; + default: + log.error('Error result unexpected'); + res.status(500).send(); + break; + } + } else { + res.status(200).send(); + } + }); + } +}); + +module.exports = router; diff --git a/routes/v1/test/status.js b/routes/v1/test/status.js new file mode 100644 index 0000000..9255b8a --- /dev/null +++ b/routes/v1/test/status.js @@ -0,0 +1,78 @@ +const express = require('express'); +const log = require('./../../../lib/logger')(__filename.slice(__dirname.length + 1)); +const qStr = require('query-string'); +const router = express.Router(); +const dblib = require('./../../../lib/dblib'); +const fs = require('fs'); + +/* GET /vN/status */ +router.get('/', function(req, res, next) { + // check header + if (typeof req.header('X-API-Key-Test') === 'undefined') { + log.info('API key not set in request header'); + res.status(401).send(); + return; + } else { + log.debug('API key set in request header'); + } + + // check query parameter + var image = true; + if (typeof req.query.image != 'undefined') { + if (req.query.image === 'false') { + image = false; + } + } + + // read settings + dblib.getSettings({ + area: 'test', + key: req.header('X-API-Key-Test') + }, (err, data) => { + if (err) { + switch (err) { + case dblib.msg.dbError: + log.info('Server error response'); + res.status(500).send(); + break; + case dblib.msg.keyMismatch: + log.info('Unauthorized access'); + res.status(401).send(); + break; + default: + log.error('Error result unexpected'); + res.status(500).send(); + break; + } + } else { + // response + var content = null; + if (image) { + var file = null; + switch (data.status.state) { + case 'open': + file = fs.readFileSync(__dirname + '/../../../public/images/open.jpg', 'base64'); + break; + case 'closed': + file = fs.readFileSync(__dirname + '/../../../public/images/closed.jpg', 'base64'); + break; + default: + log.error('Unexpected status from settings'); + res.status(500).send(); + return; + } + content = { + 'image': file, + 'state': data.status.state + }; + } else { + content = { + 'state': data.status.state + }; + } + res.json(content); + } + }); +}); + +module.exports = router; diff --git a/spec/garnod.json b/spec/garnod.json new file mode 100644 index 0000000..0c3449e --- /dev/null +++ b/spec/garnod.json @@ -0,0 +1,474 @@ +{ + "openapi": "3.0.2", + "info": { + "description": "Garage Node is inteded to be a garage door watch an control application based on a RESTful API. The (web) server is watching the door state and shall inform the user via a push notification when a garage door is open for a while. Then the user shall see the current door state and alternatively perform an motion action using a client application such as a PWA.", + "contact": { + "name": "ebelcrom" + }, + "version": "1.0.0", + "title": "Garage Node API", + "license": { + "name": "GPL 3.0", + "url": "https://www.gnu.org/licenses/gpl-3.0.txt" + } + }, + "servers": [ + { + "url": "http://localhost:3000/v1" + } + ], + "tags": [ + { + "name": "production", + "description": "Production area of this API. All requests go to the real server instance as well as all responses come from it." + }, + { + "name": "test", + "description": "Test area of this API. All requests go to a simulated server instance as well as all responses come from it." + } + ], + "paths": { + "/status": { + "get": { + "summary": "Get the current state.", + "description": "Use this call if you want to know the current state.", + "operationId": "prod_status", + "tags": [ + "production" + ], + "security": [ + { + "api_key_auth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/image" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/door_state" + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/events": { + "get": { + "summary": "Listen on events.", + "description": "For listening on events, you have to long poll this ressource. If no events occur within the timeout range, request again.

Note: This call only makes sense if you have sent a control command before.

", + "operationId": "prod_events", + "tags": [ + "production" + ], + "security": [ + { + "api_key_auth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/timeout" + }, + { + "$ref": "#/components/parameters/image" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/door_state" + }, + "304": { + "description": "No events yet, request again." + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/control": { + "post": { + "summary": "Send a command.", + "description": "After sending a command you have to poll a state change. The command result only returns the command state.", + "operationId": "prod_control", + "tags": [ + "production" + ], + "security": [ + { + "api_key_auth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/command" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/command_result" + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/test/status": { + "get": { + "summary": "Get the current state.", + "description": "Use this call if you want to know the current state.", + "operationId": "test_status", + "tags": [ + "test" + ], + "security": [ + { + "api_key_auth_test": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/image" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/door_state" + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/test/events": { + "get": { + "summary": "Listen on events.", + "description": "For listening on events, you have to long poll this ressource. If no events occur within the timeout range, request again.

Note: This call only makes sense if you have sent a control command before.

", + "operationId": "test_events", + "tags": [ + "test" + ], + "security": [ + { + "api_key_auth_test": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/timeout" + }, + { + "$ref": "#/components/parameters/image" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/door_state" + }, + "304": { + "description": "No events yet, request again" + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/test/control": { + "post": { + "summary": "Send a command.", + "description": "After sending a command you have to poll a state change. The command result only returns the command state.", + "operationId": "test_control", + "tags": [ + "test" + ], + "security": [ + { + "api_key_auth_test": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/command" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/command_result" + }, + "400": { + "$ref": "#/components/responses/error_param" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, + "/test/settings": { + "post": { + "summary": "Setup settings.", + "description": "Sets behavoir resp. response settings for test area requests.

Note: the settings are activated on the next request you send within the test area. The status is not changing until the setting are changing again. An set event only occures once.

", + "operationId": "test_set_settings", + "tags": [ + "test" + ], + "security": [ + { + "api_key_auth_test": [] + } + ], + "requestBody": { + "description": "Request body for setup settings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settings" + } + } + } + }, + "responses": { + "200": { + "description": "Ok, settings accepted" + }, + "400": { + "description": "Bad request, check settings" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + }, + "get": { + "summary": "Get current settings.", + "description": "Gets behavoir resp. response settings for test area requests.", + "operationId": "test_get_settings", + "tags": [ + "test" + ], + "security": [ + { + "api_key_auth_test": [] + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/settings_result" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + } + }, + "components": { + "schemas": { + "image": { + "description": "JPEG image of current door state.", + "type": "string", + "format": "byte" + }, + "settings": { + "description": "Setup settings.", + "type": "object", + "properties": { + "status": { + "description": "Settings for status ressource.", + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/state" + } + } + }, + "events": { + "description": "Settings for events ressource.", + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/state" + }, + "delay": { + "description": "Delay for event to occur.", + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 300 + } + } + } + } + }, + "state": { + "description": "Current door state.", + "type": "string", + "enum": [ + "closed", + "open", + "moving" + ] + } + }, + "securitySchemes": { + "api_key_auth": { + "name": "X-API-Key", + "type": "apiKey", + "in": "header", + "description": "API key for production." + }, + "api_key_auth_test": { + "name": "X-API-Key-Test", + "type": "apiKey", + "in": "header", + "description": "API key for test." + } + }, + "parameters": { + "command": { + "description": "Command to execute.", + "name": "command", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "move" + ], + "default": "move" + } + }, + "image": { + "description": "Flag saying if a image of the webcam should be returned.", + "name": "image", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } + }, + "timeout": { + "description": "Time in seconds for returning even when no events are available.", + "name": "timeout", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 300, + "default": 30 + } + } + }, + "responses": { + "command_result": { + "description": "Command result.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "description": "Current door state.", + "type": "string", + "enum": [ + "moving" + ] + } + } + } + } + } + }, + "door_state": { + "description": "Current door state.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "image": { + "$ref": "#/components/schemas/image" + }, + "state": { + "$ref": "#/components/schemas/state" + } + } + } + } + } + }, + "error_authentication": { + "description": "API key is missing or invalid.", + "headers": { + "WWW-Authenticate": { + "schema": { + "description": "API key is missing or invalid.", + "type": "string", + "enum": [ + "X-API-Key", + "X-API-Key-Test" + ] + } + } + } + }, + "error_param": { + "description": "Bad request, answer contains details", + "content": { + "application/json": { + "schema": { + "description": "One or more parameter are invalid.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "timeout", + "command" + ] + } + } + } + } + }, + "settings_result": { + "description": "Set test settings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settings" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/spec/garnod.yaml b/spec/garnod.yaml new file mode 100644 index 0000000..1083955 --- /dev/null +++ b/spec/garnod.yaml @@ -0,0 +1,323 @@ +openapi: 3.0.2 +info: + description: >- + Garage Node is inteded to be a garage door watch an control application + based on a RESTful API. The (web) server is watching the door state and + shall inform the user via a push notification when a garage door is open for + a while. Then the user shall see the current door state and alternatively + perform an motion action using a client application such as a PWA. + contact: + name: ebelcrom + version: 1.0.0 + title: Garage Node API + license: + name: GPL 3.0 + url: 'https://www.gnu.org/licenses/gpl-3.0.txt' +servers: +# - url: 'https://binomiant.duckdns.org/TYtse53t/v1' + - url: 'http://localhost:3000/v1' +tags: + - name: production + description: >- + Production area of this API. All requests go to the real server instance + as well as all responses come from it. + - name: test + description: >- + Test area of this API. All requests go to a simulated server instance as + well as all responses come from it. +paths: + /status: + get: + summary: Get the current state. + description: Use this call if you want to know the current state. + operationId: prod_status + tags: + - production + security: + - api_key_auth: [] + parameters: + - $ref: '#/components/parameters/image' + responses: + '200': + $ref: '#/components/responses/door_state' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /events: + get: + summary: Listen on events. + description: >- + For listening on events, you have to long poll this ressource. If no + events occur within the timeout range, request again.

Note: + This call only makes sense if you have sent a control command before. +

+ operationId: prod_events + tags: + - production + security: + - api_key_auth: [] + parameters: + - $ref: '#/components/parameters/timeout' + - $ref: '#/components/parameters/image' + responses: + '200': + $ref: '#/components/responses/door_state' + '304': + description: 'No events yet, request again.' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /control: + post: + summary: Send a command. + description: >- + After sending a command you have to poll a state change. The command + result only returns the command state. + operationId: prod_control + tags: + - production + security: + - api_key_auth: [] + parameters: + - $ref: '#/components/parameters/command' + responses: + '200': + $ref: '#/components/responses/command_result' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /test/status: + get: + summary: Get the current state. + description: Use this call if you want to know the current state. + operationId: test_status + tags: + - test + security: + - api_key_auth_test: [] + parameters: + - $ref: '#/components/parameters/image' + responses: + '200': + $ref: '#/components/responses/door_state' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /test/events: + get: + summary: Listen on events. + description: >- + For listening on events, you have to long poll this ressource. If no + events occur within the timeout range, request again.

Note: + This call only makes sense if you have sent a control command before. +

+ operationId: test_events + tags: + - test + security: + - api_key_auth_test: [] + parameters: + - $ref: '#/components/parameters/timeout' + - $ref: '#/components/parameters/image' + responses: + '200': + $ref: '#/components/responses/door_state' + '304': + description: 'No events yet, request again' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /test/control: + post: + summary: Send a command. + description: >- + After sending a command you have to poll a state change. The command + result only returns the command state. + operationId: test_control + tags: + - test + security: + - api_key_auth_test: [] + parameters: + - $ref: '#/components/parameters/command' + responses: + '200': + $ref: '#/components/responses/command_result' + '400': + $ref: '#/components/responses/error_param' + '401': + $ref: '#/components/responses/error_authentication' + /test/settings: + post: + summary: Setup settings. + description: >- + Sets behavoir resp. response settings for test area requests. +

Note: the settings are activated on the next request you send + within the test area. The status is not changing until the setting are + changing again. An set event only occures once.

+ operationId: test_set_settings + tags: + - test + security: + - api_key_auth_test: [] + requestBody: + description: Request body for setup settings. + content: + application/json: + schema: + $ref: '#/components/schemas/settings' + responses: + '200': + description: 'Ok, settings accepted' + '400': + description: 'Bad request, check settings' + '401': + $ref: '#/components/responses/error_authentication' + get: + summary: Get current settings. + description: Gets behavoir resp. response settings for test area requests. + operationId: test_get_settings + tags: + - test + security: + - api_key_auth_test: [] + responses: + '200': + $ref: '#/components/responses/settings_result' + '401': + $ref: '#/components/responses/error_authentication' +components: + schemas: + image: + description: JPEG image of current door state. + type: string + format: byte + settings: + description: Setup settings. + type: object + properties: + status: + description: Settings for status ressource. + type: object + properties: + state: + $ref: '#/components/schemas/state' + events: + description: Settings for events ressource. + type: object + properties: + state: + $ref: '#/components/schemas/state' + delay: + description: Delay for event to occur. + type: integer + format: int32 + minimum: 1 + maximum: 300 + state: + description: Current door state. + type: string + enum: + - closed + - open + - moving + securitySchemes: + api_key_auth: + name: X-API-Key + type: apiKey + in: header + description: API key for production. + api_key_auth_test: + name: X-API-Key-Test + type: apiKey + in: header + description: API key for test. + parameters: + command: + description: Command to execute. + name: command + in: query + schema: + type: string + enum: + - move + default: move + image: + description: Flag saying if a image of the webcam should be returned. + name: image + in: query + schema: + type: boolean + default: true + timeout: + description: Time in seconds for returning even when no events are available. + name: timeout + in: query + schema: + type: integer + format: int32 + minimum: 1 + maximum: 300 + default: 30 + responses: + command_result: + description: Command result. + content: + application/json: + schema: + type: object + required: + - state + properties: + state: + description: Current door state. + type: string + enum: + - moving + door_state: + description: Current door state. + content: + application/json: + schema: + type: object + required: + - state + properties: + image: + $ref: '#/components/schemas/image' + state: + $ref: '#/components/schemas/state' + error_authentication: + description: API key is missing or invalid. + headers: + WWW-Authenticate: + schema: + description: API key is missing or invalid. + type: string + enum: + - X-API-Key + - X-API-Key-Test + error_param: + description: 'Bad request, answer contains details' + content: + application/json: + schema: + description: One or more parameter are invalid. + type: array + items: + type: string + enum: + - image + - timeout + - command + settings_result: + description: Set test settings. + content: + application/json: + schema: + $ref: '#/components/schemas/settings' -- 2.30.2