Skip to content

Commit

Permalink
Add notifications (#1352)
Browse files Browse the repository at this point in the history
* Add Notification rule part UI

* Wire up backend for alerts
  • Loading branch information
hobinjk authored Sep 18, 2018
1 parent 5f8026d commit b248938
Show file tree
Hide file tree
Showing 20 changed files with 538 additions and 52 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"typescript": "^2.7.2",
"url-loader": "^1.0.1",
"uuid": "^3.2.1",
"web-push": "^3.3.2",
"webdriverio": "^4.12.0",
"webpack": "^4.10.2",
"webpack-cli": "^2.1.4",
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ exports.RULES_PATH = '/rules';
exports.OAUTH_PATH = '/oauth';
exports.OAUTHCLIENTS_PATH = '/authorizations';
exports.LOGS_PATH = '/logs';
exports.PUSH_PATH = '/push';
// Remember we end up in the build/* directory so these paths looks slightly
// different than you might expect.
exports.STATIC_PATH = path.join(__dirname, '../static');
Expand Down
60 changes: 60 additions & 0 deletions src/controllers/push_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Push API Controller.
*
* Implements the Push API for notifications to use
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

const PromiseRouter = require('express-promise-router');
const WebPush = require('web-push');
const Database = require('../db');
const Settings = require('../models/settings');

const PushController = PromiseRouter();

/**
* Initialize the Push API, generating and storing a VAPID keypair if necessary
*/
PushController.init = async () => {
let vapid = await Settings.get('push.vapid');
if (!vapid) {
vapid = WebPush.generateVAPIDKeys();
await Settings.set('push.vapid', vapid);
}
const {publicKey, privateKey} = vapid;

const {tunnelDomain} = await Settings.getTunnelInfo();
WebPush.setVapidDetails(tunnelDomain, publicKey, privateKey);
};

/**
* Handle requests for the public key
*/
PushController.get('/vapid-public-key', async (request, response) => {
const vapid = await Settings.get('push.vapid');
if (!vapid) {
response.status(500).json({error: 'vapid not configured'});
return;
}
response.status(200).json({publicKey: vapid.publicKey});
});

PushController.post('/register', async (request, response) => {
await Database.createPushSubscription(request.body.subscription);
response.status(200).json({});
});

PushController.broadcastNotification = async (message) => {
const subscriptions = await Database.getPushSubscriptions();
for (const subscription of subscriptions) {
WebPush.sendNotification(subscription, message).catch((err) => {
console.warn('Push API error', err);
Database.deletePushSubscription(subscription.id);
});
}
};

module.exports = PushController;
43 changes: 1 addition & 42 deletions src/controllers/settings_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,48 +239,7 @@ SettingsController.post('/skiptunnel', async (request, response) => {

SettingsController.get('/tunnelinfo', async (request, response) => {
try {
// Check to see if we have a tunnel endpoint first
const result = await Settings.get('tunneltoken');
let localDomain;
let mDNSstate;
let tunnelEndpoint;

if (typeof result === 'object') {
console.log(`Tunnel domain found. Tunnel name is: ${result.name} and`,
`tunnel domain is: ${config.get('ssltunnel.domain')}`);
tunnelEndpoint =
`https://${result.name}.${config.get('ssltunnel.domain')}`;
} else {
tunnelEndpoint = 'Not set.';
}

// Find out our default local DNS name Check for a previous name in the
// DB, if that does not exist use the default.
try {
mDNSstate = await Settings.get('multicastDNSstate');
localDomain = await Settings.get('localDNSname');
// If our DB is empty use defaults
if (typeof mDNSstate === 'undefined') {
mDNSstate = config.get(
'settings.defaults.domain.localAccess');
}
if (typeof localDomain === 'undefined') {
localDomain = config.get(
'settings.defaults.domain.localControl.mdnsServiceDomain');
}
} catch (err) {
// Catch this DB error. Since we don't know what state the mDNS process
// should be in make sure it's off
console.error(`Error getting DB entry for multicast from the DB: ${err}`);
localDomain = config.get(
'settings.defaults.domain.localControl.mdnsServiceDomain');
}

console.log(`Tunnel name is set to: ${tunnelEndpoint}`);
console.log(`Local mDNS Service Domain Name is: ${localDomain}`);
const localDomainSettings = {localDomain: localDomain,
mDNSstate: mDNSstate,
tunnelDomain: tunnelEndpoint};
const localDomainSettings = await Settings.getTunnelInfo();
response.send(localDomainSettings);
response.status(200).end();
} catch (e) {
Expand Down
54 changes: 54 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const TABLES = [
'jsonwebtokens',
'things',
'settings',
'pushSubscriptions',
];

const DEBUG = false || (process.env.NODE_ENV === 'test');
Expand Down Expand Up @@ -118,6 +119,11 @@ const Database = {
'key TEXT PRIMARY KEY,' +
'value TEXT' +
');');

this.db.run(`CREATE TABLE IF NOT EXISTS pushSubscriptions (
id INTEGER PRIMARY KEY,
subscription TEXT
);`);
},

/**
Expand Down Expand Up @@ -526,6 +532,54 @@ const Database = {
return result.changes !== 0;
},

/**
* Store a new Push subscription
* @param {Object} subscription
* @return {Promise<number>} resolves to sub id
*/
createPushSubscription: function(desc) {
return this.run(
'INSERT INTO pushSubscriptions (subscription) VALUES (?)',
[JSON.stringify(desc)]
).then((res) => {
return parseInt(res.lastID);
});
},

/**
* Get all push subscriptions
* @return {Promise<Array<PushSubscription>>}
*/
getPushSubscriptions: function() {
return new Promise((resolve, reject) => {
this.db.all(
'SELECT id, subscription FROM pushSubscriptions',
[],
function(err, rows) {
if (err) {
reject(err);
return;
}
const subs = [];
for (const row of rows) {
const sub = JSON.parse(row.subscription);
sub.id = row.id;
subs.push(sub);
}
resolve(subs);
}
);
});
},

/**
* Delete a single subscription
* @param {number} id
*/
deletePushSubscription: function(id) {
return this.run('DELETE FROM pushSubscriptions WHERE id = ?', [id]);
},

/**
* ONLY for tests (clears all tables).
*/
Expand Down
52 changes: 52 additions & 0 deletions src/models/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

'use strict';

const config = require('config');
const Database = require('../db');
const util = require('util');

Expand Down Expand Up @@ -70,6 +71,57 @@ const Settings = {
throw e;
});
},

/**
* Get an object of all tunnel settings
* @return {localDomain, mDNSstate, tunnelDomain}
*/
getTunnelInfo: async function() {
// Check to see if we have a tunnel endpoint first
const result = await Settings.get('tunneltoken');
let localDomain;
let mDNSstate;
let tunnelEndpoint;

if (typeof result === 'object') {
console.log(`Tunnel domain found. Tunnel name is: ${result.name} and`,
`tunnel domain is: ${config.get('ssltunnel.domain')}`);
tunnelEndpoint =
`https://${result.name}.${config.get('ssltunnel.domain')}`;
} else {
tunnelEndpoint = 'Not set.';
}

// Find out our default local DNS name Check for a previous name in the
// DB, if that does not exist use the default.
try {
mDNSstate = await Settings.get('multicastDNSstate');
localDomain = await Settings.get('localDNSname');
// If our DB is empty use defaults
if (typeof mDNSstate === 'undefined') {
mDNSstate = config.get(
'settings.defaults.domain.localAccess');
}
if (typeof localDomain === 'undefined') {
localDomain = config.get(
'settings.defaults.domain.localControl.mdnsServiceDomain');
}
} catch (err) {
// Catch this DB error. Since we don't know what state the mDNS process
// should be in make sure it's off
console.error(`Error getting DB entry for multicast from the DB: ${err}`);
localDomain = config.get(
'settings.defaults.domain.localControl.mdnsServiceDomain');
}

console.log(`Tunnel name is set to: ${tunnelEndpoint}`);
console.log(`Local mDNS Service Domain Name is: ${localDomain}`);
return {
localDomain: localDomain,
mDNSstate: mDNSstate,
tunnelDomain: tunnelEndpoint,
};
},
};

module.exports = Settings;
5 changes: 5 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ const Router = {
app.use(API_PREFIX + Constants.LOGS_PATH, nocache, auth,
require('./controllers/logs_controller'));

const PushController = require('./controllers/push_controller');
PushController.init().then(() => {
app.use(API_PREFIX + Constants.PUSH_PATH, nocache, auth, PushController);
});

app.use(API_PREFIX + Constants.OAUTH_PATH, nocache,
require('./controllers/oauth_controller').default);
app.use(API_PREFIX + Constants.OAUTHCLIENTS_PATH, nocache, auth,
Expand Down
51 changes: 51 additions & 0 deletions src/rules-engine/effects/NotificationEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/

const assert = require('assert');
const Effect = require('./Effect');
const PushController = require('../../controllers/push_controller');

/**
* An Effect which creates a notification
*/
class NotificationEffect extends Effect {
/**
* @param {EffectDescription} desc
*/
constructor(desc) {
super(desc);

assert(desc.hasOwnProperty('message'));

this.message = desc.message;
}

/**
* @return {EffectDescription}
*/
toDescription() {
return Object.assign(
super.toDescription(),
{
message: this.message,
}
);
}

/**
* @param {State} state
*/
setState(state) {
if (!state.on) {
return;
}

PushController.broadcastNotification(this.message);
}
}

module.exports = NotificationEffect;

1 change: 1 addition & 0 deletions src/rules-engine/effects/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const effects = {
Effect: require('./Effect'),
ActionEffect: require('./ActionEffect'),
MultiEffect: require('./MultiEffect'),
NotificationEffect: require('./NotificationEffect'),
SetEffect: require('./SetEffect'),
PulseEffect: require('./PulseEffect'),
};
Expand Down
19 changes: 13 additions & 6 deletions static/css/rule.css
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
white-space: nowrap;
}

.property-select-option {
.property-select-option, .message-input-container {
background: #4a4a4a;
height: 5rem;
line-height: 5rem;
Expand All @@ -198,14 +198,18 @@
cursor: default;
}

.property-select-name {
.property-select-name, .message-input-label {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
}

.property-select-option.selected {
.message-input-label {
overflow: visible;
}

.property-select-option.selected, .message-input-container {
position: relative;
border-radius: 0 0 1rem 0;
}
Expand Down Expand Up @@ -237,7 +241,8 @@
}

.property-select.open > .property-select-option,
.property-select-option.selected {
.property-select-option.selected,
.message-input-container {
display: flex;
align-items: center;
}
Expand All @@ -247,11 +252,13 @@
}

.property-select-option select,
.property-select-option input {
.property-select-option input,
.message-input-container input {
margin-left: 0.6rem;
}

.property-select-option input[type="text"] {
.property-select-option input[type="text"],
.message-input-container input[type="text"] {
width: 8rem;
}

Expand Down
Loading

0 comments on commit b248938

Please sign in to comment.