From 59eb6ebce5b9a6b9c70224a16ef7272fb0c3ec69 Mon Sep 17 00:00:00 2001 From: ebelcrom Date: Thu, 26 Dec 2019 22:45:51 +0100 Subject: [PATCH] test feature implemented comletly --- .gitignore | 3 + app.js | 4 + init/initcloud.js | 12 ++ init/initdb.js | 89 ++++---- init/vapid.json | 1 + lib/dblib.js | 320 +++++++++++++++++++---------- lib/logger.js | 42 ++-- package-lock.json | 312 ++++++++++++++++++++++++++++ package.json | 10 +- public/images/closed.jpg | Bin 9756 -> 22864 bytes public/images/moving.jpg | Bin 9154 -> 0 bytes public/images/open.jpg | Bin 8498 -> 22864 bytes public/index.html | 13 -- public/stylesheets/style.css | 8 - routes/index.js | 4 +- routes/v1/control.js | 4 +- routes/v1/events.js | 4 +- routes/v1/notification.js | 9 + routes/v1/status.js | 25 +-- routes/v1/test/control.js | 124 ++++++------ routes/v1/test/events.js | 182 +++++++++-------- routes/v1/test/notification.js | 82 ++++++++ routes/v1/test/settings.js | 357 ++++++++++++++++++++++----------- routes/v1/test/status.js | 128 ++++++------ spec/garnod.json | 107 +++++++++- spec/garnod.yaml | 88 ++++++-- 26 files changed, 1354 insertions(+), 574 deletions(-) create mode 100644 init/initcloud.js create mode 100644 init/vapid.json delete mode 100644 public/images/moving.jpg delete mode 100644 public/index.html delete mode 100644 public/stylesheets/style.css create mode 100644 routes/v1/notification.js create mode 100644 routes/v1/test/notification.js 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 9de6f5fc9bf2e7011241afccc75c930c5c533a6a..fb57f7d6c6a5c301581b7c4490178265e13d15ad 100644 GIT binary patch literal 22864 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 diff --git a/public/images/moving.jpg b/public/images/moving.jpg deleted file mode 100644 index 279f614981c7163369bb1d94b686b0ecf9ac8b83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9154 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 b9nA>NOT-}ihWmM=I@m%(*Jfz*FUJ25&r)(b diff --git a/public/images/open.jpg b/public/images/open.jpg index be1632b81d21593285dd68f9173052d1c667b79a..fb57f7d6c6a5c301581b7c4490178265e13d15ad 100644 GIT binary patch literal 22864 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 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: -- 2.30.2