From: ebelcrom Date: Sat, 2 Nov 2019 23:46:18 +0000 (+0100) Subject: sources added X-Git-Tag: v1.0.0~13 X-Git-Url: http://www.binomiant.duckdns.org/9wAuyR5S/?a=commitdiff_plain;h=06310a54810d779a323bb6cf1fa7b4769ae92700;p=garnod.git sources added --- 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 0000000..9de6f5f Binary files /dev/null and b/public/images/closed.jpg differ diff --git a/public/images/moving.jpg b/public/images/moving.jpg new file mode 100644 index 0000000..279f614 Binary files /dev/null and b/public/images/moving.jpg differ diff --git a/public/images/open.jpg b/public/images/open.jpg new file mode 100644 index 0000000..be1632b Binary files /dev/null and b/public/images/open.jpg differ 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'