From: ebelcrom Date: Thu, 26 Dec 2019 21:45:51 +0000 (+0100) Subject: test feature implemented comletly X-Git-Tag: v1.0.0~10 X-Git-Url: http://www.binomiant.duckdns.org/9wAuyR5S/?a=commitdiff_plain;h=59eb6ebce5b9a6b9c70224a16ef7272fb0c3ec69;p=garnod.git test feature implemented comletly --- diff --git a/.gitignore b/.gitignore index d1bed12..89dae9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# User defined +public/mVk7Yr3k/ + # Logs logs *.log diff --git a/app.js b/app.js index 23ac4a9..986a6b0 100644 --- a/app.js +++ b/app.js @@ -12,9 +12,11 @@ const indexRouter = require('./routes/index'); const prodStatus = require('./routes/' + ver + '/status'); const prodEvents = require('./routes/' + ver + '/events'); const prodControl = require('./routes/' + ver + '/control'); +const prodNotification = require('./routes/' + ver + '/notification'); const testStatus = require('./routes/' + ver + '/test/status'); const testEvents = require('./routes/' + ver + '/test/events').router; const testControl = require('./routes/' + ver + '/test/control'); +const testNotification = require('./routes/' + ver + '/test/notification'); const testSettings = require('./routes/' + ver + '/test/settings'); // api-docs @@ -51,9 +53,11 @@ app.use('/' + ver + '/spec', express.static('spec')); app.use('/' + ver + '/status', prodStatus); app.use('/' + ver + '/events', prodEvents); app.use('/' + ver + '/control', prodControl); +app.use('/' + ver + '/notification', prodNotification); app.use('/' + ver + '/test/status', testStatus); app.use('/' + ver + '/test/events', testEvents); app.use('/' + ver + '/test/control', testControl); +app.use('/' + ver + '/test/notification', testNotification); app.use('/' + ver + '/test/settings', testSettings); // api-docs diff --git a/init/initcloud.js b/init/initcloud.js new file mode 100644 index 0000000..a986f70 --- /dev/null +++ b/init/initcloud.js @@ -0,0 +1,12 @@ + + +function generateVAPIDKeys() { + var curve = crypto.createECDH('prime256v1'); + curve.generateKeys(); + + return { + publicKey: curve.getPublicKey(), + privateKey: curve.getPrivateKey(), + }; +} + diff --git a/init/initdb.js b/init/initdb.js index d95b06c..fd9e4e1 100644 --- a/init/initdb.js +++ b/init/initdb.js @@ -5,57 +5,68 @@ const name = 'garnod'; // create db mongo.connect(url + '/' + name, function(err, db) { if (err) { - throw err; - return; - } + throw err; + return; + } console.log('garnod created!'); - // create collections + // create collections var dbo = db.db(name); dbo.createCollection('keys', function(err, res) { if (err) { - throw err; - return; - } + 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!'); - }); + // 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('notifications', function(err, res) { + if (err) { + throw err; + return; + } + console.log('notifications created!'); }); dbo.createCollection('settings', function(err, res) { if (err) { - throw err; - return; - } + 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!'); - }); + // populate collection + dbo.collection('settings').insertOne({ + _id: 1, + status: { + state: 'closed' + }, + events: { + state: 'closed', + delay: 0 + }, + notification: { + delay: 0 + } + }, function(err, res) { + if (err) { + throw err; + return; + } + console.log('settings set!'); + }); }); }); diff --git a/init/vapid.json b/init/vapid.json new file mode 100644 index 0000000..530596e --- /dev/null +++ b/init/vapid.json @@ -0,0 +1 @@ +{"publicKey":"BLDSdGasI5sLks30brbIWvlLMFqzoxxkOs7aW_E9PDBzIO_mDs6-tvtb2U0-BVFDafNd58DJgoXxdK5711FF29c","privateKey":"_AFTIegzYV_l_5RYwzOCc22cpYcMUmpkA8bLbrlNq9I"} diff --git a/lib/dblib.js b/lib/dblib.js index a0cc9bd..8d35a61 100644 --- a/lib/dblib.js +++ b/lib/dblib.js @@ -5,9 +5,9 @@ const name = 'garnod'; var db = null; const msg = { - ok: 'ok', - dbError: 'dbError', - keyMismatch: 'keyMismatch' + ok: 'ok', + dbError: 'dbError', + keyMismatch: 'keyMismatch' }; function connect() { @@ -33,113 +33,221 @@ function connect() { 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); - }); + // 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', JSON.stringify(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', JSON.stringify(err)); + return callback(err, null); + }) + + // return result + .then(res => { + return callback(null, res); + }); +} + +function setNotification(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', JSON.stringify(err)); + return callback(err, null); + }) + + // save notification data + .then(res => { + return new Promise((resolve, reject) => { + var settings = db.db(name).collection('notifications'); + log.debug('Update notifications:', JSON.stringify(data.notification)); + settings.updateOne({ _id: 1 }, { $set: data.notification }, { upsert: true }, (err, res) => { + if (err) { + log.error('Error on updating notification', JSON.stringify(err)); + return reject(msg.dbError); + } + log.debug('Update notification done'); + resolve(msg.ok); + }); + }); + }) + .catch (err => { + log.error('Error on updating notification', JSON.stringify(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); - }); + // 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', JSON.stringify(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); + }); +} + +function getNotification(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', JSON.stringify(err)); + return callback(err, null); + }) + + // get notification + .then(res => { + return new Promise((resolve, reject) => { + var settings = db.db(name).collection('notifications'); + log.debug('Search for notification object'); + settings.findOne({ _id: 1 }, {}, (err, res) => { + if (err) { + log.error('Error on getting notification', JSON.stringify(err)); + return reject(msg.dbError); + } + log.debug('Get notification done:', JSON.stringify(res)); + resolve(res); + }); + }); + }) + .catch (err => { + log.error('Error on getting notification', JSON.stringify(err)); + return callback(err, null); + }) + + // return result + .then(res => { + return callback(null, res); + }); } module.exports = { - msg, - setSettings, - getSettings + msg, + setSettings, + getSettings, + setNotification, + getNotification } diff --git a/lib/logger.js b/lib/logger.js index c8b145b..c0f145e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -42,26 +42,26 @@ const logger = new (winston.Logger) ({ }); 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); - }, - } + 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 + return myLogger } diff --git a/package-lock.json b/package-lock.json index 394f367..78be1d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,22 @@ "negotiator": "0.6.2" } }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -28,6 +44,16 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, + "asn1.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz", + "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "async": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", @@ -47,6 +73,11 @@ "safe-buffer": "5.1.2" } }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -79,6 +110,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", @@ -104,6 +140,67 @@ "quick-lru": "^1.0.0" } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cliff": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/cliff/-/cliff-0.1.10.tgz", + "integrity": "sha1-U74z6p9ZvshWCe4wCsQgdgPlIBM=", + "requires": { + "colors": "~1.0.3", + "eyes": "~0.1.8", + "winston": "0.8.x" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "winston": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", + "integrity": "sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=", + "requires": { + "async": "0.2.x", + "colors": "0.6.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "pkginfo": "0.3.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" + } + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", @@ -221,6 +318,14 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -250,11 +355,31 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + }, + "dependencies": { + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + } + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -357,11 +482,26 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "homedir": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/homedir/-/homedir-0.6.0.tgz", + "integrity": "sha1-KyHbZr8Ipts4JJo+/1LX0YcGrx4=" + }, "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==" }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -373,6 +513,38 @@ "statuses": ">= 1.4.0 < 2" } }, + "http_ece": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz", + "integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==", + "requires": { + "urlsafe-base64": "~1.0.0" + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -421,6 +593,25 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -441,6 +632,11 @@ "path-exists": "^3.0.0" } }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -450,6 +646,15 @@ "signal-exit": "^3.0.0" } }, + "m3u8stream": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.6.2.tgz", + "integrity": "sha512-WsuM2bd5pPN80xvfrB+1DZqr4M7+kJl8byi6+ZCy6cmVjEiHhmr/desN53Ngsa6Hs13kYumeVgT4wL0oIJ+v6g==", + "requires": { + "miniget": "^1.4.0", + "sax": "^1.2.4" + } + }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -504,6 +709,16 @@ "mime-db": "1.40.0" } }, + "miniget": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-1.6.1.tgz", + "integrity": "sha512-I5oBwZmcaOuJrjQn7lpS29HM+aAZDbzKbX5ouxVyhFYdg6fA6YKOTwOCgzZQwlHuMek3FlCxz6eNrd4pOXbwOA==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -675,6 +890,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "progress-bar": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/progress-bar/-/progress-bar-0.1.1.tgz", + "integrity": "sha1-wyg0+I8PjqxGxjjg5jjxShwXvR8=" + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -794,6 +1014,19 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -912,6 +1145,14 @@ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, "swagger-ui-dist": { "version": "3.23.11", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.11.tgz", @@ -930,6 +1171,14 @@ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -944,6 +1193,16 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "urlsafe-base64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", + "integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY=" + }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -968,6 +1227,26 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "web-push": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.4.1.tgz", + "integrity": "sha512-wtx18llPtWWW+x8hv+Gxvz+2VjO+vZuyihInsjySNpNGNVswH1Bb2KkbbCtE96yi52VUmbFMdidxM8kJAPaSWQ==", + "requires": { + "asn1.js": "^5.0.0", + "http_ece": "1.1.0", + "https-proxy-agent": "^3.0.0", + "jws": "^3.1.3", + "minimist": "^1.2.0", + "urlsafe-base64": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "winston": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/winston/-/winston-1.1.2.tgz", @@ -989,6 +1268,39 @@ "requires": { "camelcase": "^4.1.0" } + }, + "ytdl": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/ytdl/-/ytdl-0.13.1.tgz", + "integrity": "sha512-+CJ4xjmMhCcL3vNLmg30TiFjeem7EZ5IbMzp1Ciaauyf5Lb+oe0S7JJotJ/FRRQCBlcsV8R8sv9s+KGNW4Nvng==", + "requires": { + "chalk": "^2.0.0", + "cliff": "~0.1.8", + "commander": "^3.0.0", + "homedir": "^0.6.0", + "lodash.throttle": "^4.1.1", + "progress-bar": "~0.1.1", + "sanitize-filename": "^1.6.1", + "ytdl-core": "^1.0.0" + }, + "dependencies": { + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + } + } + }, + "ytdl-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-1.0.0.tgz", + "integrity": "sha512-aP/UBWbZtBSYlqHRYOx1oZ4tMRCAk2I5Y+OJy20/hrrr5XOhgxZ6+vJ20397h1FxYRzOTRSwb3VpBs3/CLe3fA==", + "requires": { + "html-entities": "^1.1.3", + "m3u8stream": "^0.6.2", + "miniget": "^1.6.0", + "sax": "^1.1.3" + } } } } diff --git a/package.json b/package.json index 0d2aab9..56ab0b7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "garnod", - "version": "0.0.0", - "description": "A garage door monitoring application.", + "version": "1.0.0", + "description": "A garage door monitoring application (server).", "scripts": { - "start": "node ./bin/www" + "start": "node ./bin/garnod" }, "author": "ebelcrom", "license": "GPL-2.0-or-later", @@ -16,7 +16,9 @@ "morgan": "~1.9.1", "query-string": "^6.8.3", "swagger-ui-express": "^4.1.2", - "winston": "1.1.2" + "web-push": "^3.4.1", + "winston": "1.1.2", + "ytdl": "^0.13.1" }, "devDependencies": { "express-generator": "^4.16.1" diff --git a/public/images/closed.jpg b/public/images/closed.jpg index 9de6f5f..fb57f7d 100644 Binary files a/public/images/closed.jpg and b/public/images/closed.jpg differ diff --git a/public/images/moving.jpg b/public/images/moving.jpg deleted file mode 100644 index 279f614..0000000 Binary files a/public/images/moving.jpg and /dev/null differ diff --git a/public/images/open.jpg b/public/images/open.jpg index be1632b..fb57f7d 100644 Binary files a/public/images/open.jpg and b/public/images/open.jpg differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index ab1ad8a..0000000 --- a/public/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Express - - - - -

Express

-

Welcome to Express

- - - diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css deleted file mode 100644 index 9453385..0000000 --- a/public/stylesheets/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; -} - -a { - color: #00B7FF; -} diff --git a/routes/index.js b/routes/index.js index ecca96a..1e2de67 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,9 @@ var express = require('express'); var router = express.Router(); -/* GET home page. */ +/* GET / */ router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); + res.render('index'); }); module.exports = router; diff --git a/routes/v1/control.js b/routes/v1/control.js index ecca96a..8cc3399 100644 --- a/routes/v1/control.js +++ b/routes/v1/control.js @@ -1,9 +1,9 @@ var express = require('express'); var router = express.Router(); -/* GET home page. */ +/* GET */ router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); + res.status(500).send; }); module.exports = router; diff --git a/routes/v1/events.js b/routes/v1/events.js index ecca96a..8cc3399 100644 --- a/routes/v1/events.js +++ b/routes/v1/events.js @@ -1,9 +1,9 @@ var express = require('express'); var router = express.Router(); -/* GET home page. */ +/* GET */ router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); + res.status(500).send; }); module.exports = router; diff --git a/routes/v1/notification.js b/routes/v1/notification.js new file mode 100644 index 0000000..8cc3399 --- /dev/null +++ b/routes/v1/notification.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET */ +router.get('/', function(req, res, next) { + res.status(500).send; +}); + +module.exports = router; diff --git a/routes/v1/status.js b/routes/v1/status.js index 23e5332..8cc3399 100644 --- a/routes/v1/status.js +++ b/routes/v1/status.js @@ -1,30 +1,9 @@ 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 */ +/* GET */ 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' ]); - } + res.status(500).send; }); module.exports = router; diff --git a/routes/v1/test/control.js b/routes/v1/test/control.js index 21821cc..d546a38 100644 --- a/routes/v1/test/control.js +++ b/routes/v1/test/control.js @@ -10,78 +10,78 @@ 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 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); - } + // 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(); - } - }); + // 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'); + 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); + 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 index f39d2ff..95830e9 100644 --- a/routes/v1/test/events.js +++ b/routes/v1/test/events.js @@ -10,109 +10,117 @@ const events = new EventEmitter(); var response = null; var request = null; var timer = null; -const delayMin = 1; +const delayMin = -1; const delayMax = 300; +function nocache(req, res, next) { + res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); + res.header('Expires', '-1'); + res.header('Pragma', 'no-cache'); + next(); +} + /* 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'); - } +router.get('/', nocache, 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? + // 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; - } - } - } + // 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'); - } + // schedule resonse on timeout + response = res; + request = req; + if (timer === null && timeout !== delayMin) { + 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; - } + 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; - } + // 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? + // 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; - } + // 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; + log.debug('Timeout, no events'); + clearTimeout(timer); + timer = null; +//console.log('response:', response); + response.status(304).end(''); + response = null; } module.exports = { - router, - events + router, + events } diff --git a/routes/v1/test/notification.js b/routes/v1/test/notification.js new file mode 100644 index 0000000..dc866db --- /dev/null +++ b/routes/v1/test/notification.js @@ -0,0 +1,82 @@ +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'); + +/* POST /vN/test/notification */ +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{ + // check endpoint + if (typeof req.body.endpoint != 'string' ) { + log.info('Type of endpoint in content invalid'); + res.status(400).send(); + return; + } + + // check expirationTime + if (req.body.expirationTime !== null) { + if (typeof req.body.expirationTime != 'string' ) { + log.info('Type of expirationTime in content invalid'); + res.status(400).send(); + return; + } + } + + // check keys + if (typeof req.body.keys == 'object' ) { + if (typeof req.body.keys.p256dh != 'string' ) { + log.info('Type of keys.p256dh in content invalid'); + res.status(400).send(); + return; + } + if (typeof req.body.keys.auth != 'string' ) { + log.info('Type of keys.auth in content invalid'); + res.status(400).send(); + return; + } + } + + // save to DB + dblib.setNotification({ + area: 'test', + key: req.header('X-API-Key-Test'), + notification: req.body + }, (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/settings.js b/routes/v1/test/settings.js index 07e3910..871fee7 100644 --- a/routes/v1/test/settings.js +++ b/routes/v1/test/settings.js @@ -3,134 +3,255 @@ const log = require('./../../../lib/logger')(__filename.slice(__dirname.length + const qStr = require('query-string'); const router = express.Router(); const dblib = require('./../../../lib/dblib'); +const webpush = require('web-push'); -const delayMin = 1; +const delayMin = -1; const delayMax = 300; +var timer = null; /* 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 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 = {}; + // 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 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)); + // 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; + } + } - // check settings - if (Object.getOwnPropertyNames(settings).length === 0) { - log.info('Settings is empty'); - res.status(400).send(); - return; - } + // populate notification + if (typeof req.body.notification == 'object' ) { + if (typeof req.body.notification.delay == 'number' ) { + var delay = parseInt(req.body.notification.delay); + if (delay < delayMin || delay > delayMax) { + log.info('Value of notification.delay in content not in range'); + res.status(400).send(); + return; + } else { + settings.notification = { + delay: delay + }; + } + } else { + log.info('Type of notification.delay in content invalid'); + res.status(400).send(); + return; + } + } + log.debug('Parsed settings:', JSON.stringify(settings)); - // 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(); - } - }); - } + // 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 { + // notify + if (typeof req.body.notification == 'object' ) { + if (typeof req.body.notification.delay == 'number' && + req.body.notification.delay !== delayMin) { + notify(req, res, next, settings.notification.delay); + } else { + res.status(200).send(); + } + } + } + }); + } }); +function notify(req, res, next, delay) { + // get settings + dblib.getNotification({ + 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 + scheduleNotification(delay, data); + res.status(200).send(); + } + }); +} + +function scheduleNotification(delay, notification) { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + timer = setTimeout(executeNotification, delay * 1000, notification); + log.debug('Notification set to execute in ' + delay + ' seconds'); +} + +/* +{ + "endpoint":"https://fcm.googleapis.com/fcm/send/eGS6ZOFQcUk:APA91bGZliEmCLdFcuytpz2KFdLZVNm4YwAdbjaRxyF8tl6vhmuNd7L0NZagys77AjA3GWc3RNhad3i3Bc3rwBQEPKfAX4LZ0jOPzo_heG-WL7MNZx5w9lI2qycrdNk8UiQS0IlhL-j5", + "expirationTime":null, + "keys":{ + "p256dh":"BAMu7XxsCuoB0j1zdaNGXoRterzytIpyG7yxzfEnAaA0SF4xTWJOztnbUoKX4IBdKpteJA9UhhyLI286mZKUlTQ", + "auth":"iPebCb94XG_zeVdfAmKN8Q" + } +} +*/ +function executeNotification(notification) { + clearTimeout(timer); + timer = null; + + // web push + webpush.setGCMAPIKey('AAAAUtHuYco:APA91bEBTxCRGaez9_glljXAlit3PY5HMwhLSqWYMC1j-jFSp6nvnNqjI42jAVFApQbM0oyAOQjCUilIovB76cwTFxyZTP96wm9n09XwiMRXJjhwiJX1hO32mBB2zwK6X-w7epE1V67K'); + webpush.setVapidDetails( + 'mailto:ebelcrom@gmail.com', + 'BLDSdGasI5sLks30brbIWvlLMFqzoxxkOs7aW_E9PDBzIO_mDs6-tvtb2U0-BVFDafNd58DJgoXxdK5711FF29c', + '_AFTIegzYV_l_5RYwzOCc22cpYcMUmpkA8bLbrlNq9I' + ); + + log.debug('Notification data:', JSON.stringify(notification)) + const pushSubscription = { + endpoint: notification.endpoint, + keys: { + auth: notification.keys.auth, + p256dh: notification.keys.p256dh + } + }; + const options = { + actions: [ + { + action: 'Open Garage Node', + title: 'Garage Node' + } + ], + body: 'The garage door is open!', + badge: './img/icons/android-chrome-192x192.png', + icon: './img/icons/android-chrome-192x192.png' + }; + + webpush.sendNotification(pushSubscription, 'The garage door is open!') + .then(data => { + if (data) { + log.info('sent, data:', JSON.stringify(data)); + } + }) + .catch(err => { + log.info('sent, err:', JSON.stringify(err)); + }); +} + module.exports = router; diff --git a/routes/v1/test/status.js b/routes/v1/test/status.js index 9255b8a..75b025d 100644 --- a/routes/v1/test/status.js +++ b/routes/v1/test/status.js @@ -7,72 +7,72 @@ 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 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; - } - } + // 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); - } - }); + // 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 index 0c3449e..1c31f6b 100644 --- a/spec/garnod.json +++ b/spec/garnod.json @@ -1,7 +1,7 @@ { "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.", + "description": "Garage Node is inteded to be a garage door watch and 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" }, @@ -14,17 +14,17 @@ }, "servers": [ { - "url": "http://localhost:3000/v1" + "url": "https://binomiant.duckdns.org/mVk7Yr3k/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." + "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." + "description": "Test area of this API. All requests go to a simulated server instance as well as all responses come from it." } ], "paths": { @@ -62,7 +62,7 @@ "/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.

", + "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" @@ -99,7 +99,7 @@ "/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.", + "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" @@ -127,6 +127,42 @@ } } }, + "/notification": { + "post": { + "summary": "Setup web push notification.", + "description": "To be able to send web push notifications the server has to have subscription data from the client.", + "operationId": "prod_notification", + "tags": [ + "production" + ], + "security": [ + { + "api_key_auth": [] + } + ], + "requestBody": { + "description": "Request body for setup push notification.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/notification" + } + } + } + }, + "responses": { + "200": { + "description": "Ok, settings accepted" + }, + "400": { + "description": "Bad request, check request body" + }, + "401": { + "$ref": "#/components/responses/error_authentication" + } + } + } + }, "/test/status": { "get": { "summary": "Get the current state.", @@ -161,7 +197,7 @@ "/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.

", + "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" @@ -198,7 +234,7 @@ "/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.", + "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" @@ -312,13 +348,62 @@ "$ref": "#/components/schemas/state" }, "delay": { - "description": "Delay for event to occur.", + "description": "Delay for event to occur. If set to -1 the event is disabled.\n", "type": "integer", "format": "int32", - "minimum": 1, + "minimum": -1, "maximum": 300 } } + }, + "notification": { + "description": "Settings for push notification of an open door.\n", + "type": "object", + "properties": { + "delay": { + "description": "Delay for the notification to occur. If set to -1 the notification is\ndisabled.\n", + "type": "integer", + "format": "int32", + "minimum": -1, + "maximum": 300 + } + } + } + } + }, + "notification": { + "description": "Notification Data.", + "type": "object", + "required": [ + "endpoint", + "expirationTime", + "keys" + ], + "properties": { + "endpoint": { + "description": "Push service endpoint.", + "type": "string" + }, + "expirationTime": { + "description": "Expiration time of subscription.", + "type": "string" + }, + "keys": { + "type": "object", + "required": [ + "p256dh", + "auth" + ], + "properties": { + "p256dh": { + "description": "An elliptic curve Diffie–Hellman public key on the P-256 curve.", + "type": "string" + }, + "auth": { + "description": "An authentication secret.", + "type": "string" + } + } } } }, @@ -369,7 +454,7 @@ } }, "timeout": { - "description": "Time in seconds for returning even when no events are available.", + "description": "Time in seconds for returning even when no events are available.\n", "name": "timeout", "in": "query", "schema": { diff --git a/spec/garnod.yaml b/spec/garnod.yaml index 1083955..5f7374e 100644 --- a/spec/garnod.yaml +++ b/spec/garnod.yaml @@ -1,7 +1,7 @@ openapi: 3.0.2 info: description: >- - Garage Node is inteded to be a garage door watch an control application + Garage Node is inteded to be a garage door watch and 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 @@ -14,16 +14,15 @@ info: 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' + - url: 'https://binomiant.duckdns.org/mVk7Yr3k/v1' tags: - name: production description: >- - Production area of this API. All requests go to the real server instance + 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 + Test area of this API. All requests go to a simulated server instance as well as all responses come from it. paths: /status: @@ -48,7 +47,7 @@ paths: get: summary: Listen on events. description: >- - For listening on events, you have to long poll this ressource. If no + 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.

@@ -73,7 +72,7 @@ paths: post: summary: Send a command. description: >- - After sending a command you have to poll a state change. The command + After sending a command you have to poll a state change. The command result only returns the command state. operationId: prod_control tags: @@ -89,6 +88,30 @@ paths: $ref: '#/components/responses/error_param' '401': $ref: '#/components/responses/error_authentication' + /notification: + post: + summary: Setup web push notification. + description: >- + To be able to send web push notifications the server has to have + subscription data from the client. + operationId: prod_notification + tags: + - production + security: + - api_key_auth: [] + requestBody: + description: Request body for setup push notification. + content: + application/json: + schema: + $ref: '#/components/schemas/notification' + responses: + '200': + description: 'Ok, settings accepted' + '400': + description: 'Bad request, check request body' + '401': + $ref: '#/components/responses/error_authentication' /test/status: get: summary: Get the current state. @@ -111,7 +134,7 @@ paths: get: summary: Listen on events. description: >- - For listening on events, you have to long poll this ressource. If no + 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.

@@ -136,7 +159,7 @@ paths: post: summary: Send a command. description: >- - After sending a command you have to poll a state change. The command + After sending a command you have to poll a state change. The command result only returns the command state. operationId: test_control tags: @@ -214,11 +237,51 @@ components: state: $ref: '#/components/schemas/state' delay: - description: Delay for event to occur. + description: | + Delay for event to occur. If set to -1 the event is disabled. + type: integer + format: int32 + minimum: -1 + maximum: 300 + notification: + description: | + Settings for push notification of an open door. + type: object + properties: + delay: + description: | + Delay for the notification to occur. If set to -1 the notification is + disabled. type: integer format: int32 - minimum: 1 + minimum: -1 maximum: 300 + notification: + description: Notification Data. + type: object + required: + - endpoint + - expirationTime + - keys + properties: + endpoint: + description: Push service endpoint. + type: string + expirationTime: + description: Expiration time of subscription. + type: string + keys: + type: object + required: + - p256dh + - auth + properties: + p256dh: + description: An elliptic curve Diffie–Hellman public key on the P-256 curve. + type: string + auth: + description: An authentication secret. + type: string state: description: Current door state. type: string @@ -255,7 +318,8 @@ components: type: boolean default: true timeout: - description: Time in seconds for returning even when no events are available. + description: | + Time in seconds for returning even when no events are available. name: timeout in: query schema: