Implemented reminders
Added IP filters
This commit is contained in:
parent
2a39aabaa0
commit
9e226013dc
@ -2,6 +2,10 @@ module.exports = {
|
|||||||
port: 3369,
|
port: 3369,
|
||||||
publicUrl: 'https://your.domain.name/',
|
publicUrl: 'https://your.domain.name/',
|
||||||
authToken: '<token required to push notifications>',
|
authToken: '<token required to push notifications>',
|
||||||
|
allowedIps: {
|
||||||
|
post: ['127.0.0.1'],
|
||||||
|
manage: null
|
||||||
|
},
|
||||||
|
|
||||||
dataFilename: './data.json',
|
dataFilename: './data.json',
|
||||||
salt: '<generate a random string of characters>',
|
salt: '<generate a random string of characters>',
|
||||||
@ -10,7 +14,8 @@ module.exports = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
interval: { days: 1 },
|
interval: { days: 1 },
|
||||||
title: 'Reminder',
|
title: 'Reminder',
|
||||||
message: 'One or more notifications are still disabled'
|
message: 'Notifications for \'{title}\' are still disabled. Enable the notifications or disable the reminders.',
|
||||||
|
sound: 'vibrate'
|
||||||
},
|
},
|
||||||
|
|
||||||
contacts: [
|
contacts: [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
notification: {
|
notification: {
|
||||||
loading: 'Loading, please wait...',
|
loading: 'Loading, please wait...',
|
||||||
tokenInvalid: 'Invalid token',
|
tokenInvalid: 'Invalid token or access denied',
|
||||||
listHeader: 'Disabled notifications',
|
listHeader: 'Disabled notifications',
|
||||||
|
|
||||||
latchTime: 'Sent on {latchTime}',
|
latchTime: 'Sent on {latchTime}',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
notification: {
|
notification: {
|
||||||
loading: 'Bezig met laden, een ogenblik geduld a.u.b....',
|
loading: 'Bezig met laden, een ogenblik geduld a.u.b....',
|
||||||
tokenInvalid: 'Ongeldig token',
|
tokenInvalid: 'Ongeldig token of geen toegang',
|
||||||
listHeader: 'Uitgeschakelde meldingen',
|
listHeader: 'Uitgeschakelde meldingen',
|
||||||
|
|
||||||
latchTime: 'Verzonden op {latchTime}',
|
latchTime: 'Verzonden op {latchTime}',
|
||||||
|
7
index.js
7
index.js
@ -9,6 +9,7 @@ const NotificationRepository = require('./src/notification/repository');
|
|||||||
const NotificationFacade = require('./src/notification/facade');
|
const NotificationFacade = require('./src/notification/facade');
|
||||||
const TransportProvider = require('./src/transport/provider');
|
const TransportProvider = require('./src/transport/provider');
|
||||||
const SubjectParser = require('./src/subjectparser');
|
const SubjectParser = require('./src/subjectparser');
|
||||||
|
const ReminderScheduler = require('./src/reminderscheduler');
|
||||||
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
@ -17,7 +18,7 @@ const logger = winston.createLogger({
|
|||||||
|
|
||||||
logger.add(new winston.transports.Console({
|
logger.add(new winston.transports.Console({
|
||||||
format: winston.format.simple()
|
format: winston.format.simple()
|
||||||
}))
|
}));
|
||||||
|
|
||||||
|
|
||||||
const container = new Container();
|
const container = new Container();
|
||||||
@ -29,6 +30,7 @@ container.registerType('NotificationRepository', NotificationRepository);
|
|||||||
container.registerType('NotificationFacade', NotificationFacade);
|
container.registerType('NotificationFacade', NotificationFacade);
|
||||||
container.registerType('TransportProvider', TransportProvider);
|
container.registerType('TransportProvider', TransportProvider);
|
||||||
container.registerType('SubjectParser', SubjectParser);
|
container.registerType('SubjectParser', SubjectParser);
|
||||||
|
container.registerType('ReminderScheduler', ReminderScheduler);
|
||||||
|
|
||||||
|
|
||||||
async function asyncMain()
|
async function asyncMain()
|
||||||
@ -51,6 +53,7 @@ async function asyncMain()
|
|||||||
|
|
||||||
|
|
||||||
await container.NotificationRepository.init();
|
await container.NotificationRepository.init();
|
||||||
|
container.ReminderScheduler.start();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -60,7 +63,7 @@ async function asyncMain()
|
|||||||
app.use('/api', container.ApiRoutes.createRouter(express));
|
app.use('/api', container.ApiRoutes.createRouter(express));
|
||||||
app.use('/', express.static('frontend/dist'));
|
app.use('/', express.static('frontend/dist'));
|
||||||
|
|
||||||
app.listen(config.port, () =>
|
app.listen(config.port, '0.0.0.0', () =>
|
||||||
{
|
{
|
||||||
logger.info(`NotificationLatch listening at http://localhost:${config.port}`);
|
logger.info(`NotificationLatch listening at http://localhost:${config.port}`);
|
||||||
});
|
});
|
||||||
|
@ -78,6 +78,12 @@ class NotificationFacade
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async sendReminders(interval, title, message, sound)
|
||||||
|
{
|
||||||
|
await this.notificationRepository.processReminderNotifications(interval, this._sendReminder.bind(this, title, message, sound));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_sendNotification(notification)
|
_sendNotification(notification)
|
||||||
{
|
{
|
||||||
this.contacts.forEach(contact =>
|
this.contacts.forEach(contact =>
|
||||||
@ -88,6 +94,25 @@ class NotificationFacade
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_sendReminder(title, message, sound, token, notification)
|
||||||
|
{
|
||||||
|
const reminderNotification = {
|
||||||
|
id: notification.id,
|
||||||
|
token: token,
|
||||||
|
title: title,
|
||||||
|
message: message.replace('{title}', notification.title),
|
||||||
|
priority: 0,
|
||||||
|
timestamp: this.dateTimeProvider.unixTimestamp(),
|
||||||
|
url: new URL('/#/n/' + token, this.publicUrl).href
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sound)
|
||||||
|
reminderNotification.sound = sound;
|
||||||
|
|
||||||
|
this._sendNotification(reminderNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_delay(ms)
|
_delay(ms)
|
||||||
{
|
{
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
@ -133,6 +133,32 @@ class NotificationRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async processReminderNotifications(interval, callback)
|
||||||
|
{
|
||||||
|
this._checkInitialized();
|
||||||
|
|
||||||
|
const now = this.dateTimeProvider.unixTimestamp();
|
||||||
|
let shouldFlush = false;
|
||||||
|
|
||||||
|
Object.keys(this.notifications).forEach(token =>
|
||||||
|
{
|
||||||
|
const notification = this.notifications[token];
|
||||||
|
|
||||||
|
if (!notification.latched || !notification.reminders || (notification.remindTime !== null && (now - notification.remindTime) < interval))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.logger.info(`Sending reminder for notification with id '${notification.id}' and token '${token}'`)
|
||||||
|
|
||||||
|
callback(token, notification);
|
||||||
|
notification.remindTime = now;
|
||||||
|
shouldFlush = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldFlush)
|
||||||
|
await this._flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async setReminders(token, enabled)
|
async setReminders(token, enabled)
|
||||||
{
|
{
|
||||||
this._checkInitialized();
|
this._checkInitialized();
|
||||||
|
58
src/reminderscheduler.js
Normal file
58
src/reminderscheduler.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const { Duration } = require('luxon');
|
||||||
|
|
||||||
|
|
||||||
|
class ReminderScheduler
|
||||||
|
{
|
||||||
|
static create = container => new this(container.Config, container.Logger, container.NotificationFacade);
|
||||||
|
|
||||||
|
constructor(config, logger, notificationFacade)
|
||||||
|
{
|
||||||
|
this.reminders = config.reminders;
|
||||||
|
this.logger = logger;
|
||||||
|
this.notificationFacade = notificationFacade;
|
||||||
|
|
||||||
|
this.timerInterval = 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start()
|
||||||
|
{
|
||||||
|
if (!this.reminders.enabled)
|
||||||
|
{
|
||||||
|
this.logger.info('Reminders are disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.interval = Duration.fromObject(this.reminders.interval).shiftTo('seconds').seconds;
|
||||||
|
if (this.interval <= 0)
|
||||||
|
{
|
||||||
|
this.logger.warn(`Invalid reminder interval: ${this.interval} seconds, reminders will NOT be sent`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Checking for reminders every minute, interval is ${this.interval} seconds`);
|
||||||
|
setTimeout(this._onTimer.bind(this), this.timerInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async _onTimer()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.logger.verbose('Checking for reminders');
|
||||||
|
await this.notificationFacade.sendReminders(this.interval, this.reminders.title, this.reminders.message, this.reminders.sound);
|
||||||
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
|
this.logger.error(`Error while sending reminders: ${err}`);
|
||||||
|
this.logger.verbose(err.stack);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
setTimeout(this._onTimer.bind(this), this.timerInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = ReminderScheduler;
|
@ -8,6 +8,7 @@ class ApiRoutes
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.notificationFacade = notificationFacade;
|
this.notificationFacade = notificationFacade;
|
||||||
this.authToken = config.authToken;
|
this.authToken = config.authToken;
|
||||||
|
this.allowedIps = config.allowedIps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ class ApiRoutes
|
|||||||
router.post('/notification/:token/reset', this._wrapAsyncHandler(this._handleResetNotification));
|
router.post('/notification/:token/reset', this._wrapAsyncHandler(this._handleResetNotification));
|
||||||
router.post('/notification/:token/reminders/enable', this._wrapAsyncHandler(this._handleEnableReminders));
|
router.post('/notification/:token/reminders/enable', this._wrapAsyncHandler(this._handleEnableReminders));
|
||||||
router.post('/notification/:token/reminders/disable', this._wrapAsyncHandler(this._handleDisableReminders));
|
router.post('/notification/:token/reminders/disable', this._wrapAsyncHandler(this._handleDisableReminders));
|
||||||
|
router.get('/ip', this._handleIp.bind(this));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@ -27,6 +29,13 @@ class ApiRoutes
|
|||||||
|
|
||||||
async _handlePostNotification(req, res)
|
async _handlePostNotification(req, res)
|
||||||
{
|
{
|
||||||
|
if (!this._isIpAllowed(req, this.allowedIps.post))
|
||||||
|
{
|
||||||
|
this._logRequestWarning(req, 'IP address not in allowedIps.post');
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.headers.authorization !== 'Bearer ' + this.authToken)
|
if (req.headers.authorization !== 'Bearer ' + this.authToken)
|
||||||
{
|
{
|
||||||
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
||||||
@ -48,6 +57,13 @@ class ApiRoutes
|
|||||||
|
|
||||||
async _handleResetNotification(req, res)
|
async _handleResetNotification(req, res)
|
||||||
{
|
{
|
||||||
|
if (!this._isIpAllowed(req, this.allowedIps.manage))
|
||||||
|
{
|
||||||
|
this._logRequestWarning(req, 'IP address not in allowedIps.manage');
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.notificationFacade.resetNotification(req.params.token);
|
await this.notificationFacade.resetNotification(req.params.token);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}
|
}
|
||||||
@ -55,6 +71,13 @@ class ApiRoutes
|
|||||||
|
|
||||||
async _handleEnableReminders(req, res)
|
async _handleEnableReminders(req, res)
|
||||||
{
|
{
|
||||||
|
if (!this._isIpAllowed(req, this.allowedIps.manage))
|
||||||
|
{
|
||||||
|
this._logRequestWarning(req, 'IP address not in allowedIps.manage');
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.notificationFacade.setReminders(req.params.token, true);
|
await this.notificationFacade.setReminders(req.params.token, true);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}
|
}
|
||||||
@ -62,6 +85,13 @@ class ApiRoutes
|
|||||||
|
|
||||||
async _handleDisableReminders(req, res)
|
async _handleDisableReminders(req, res)
|
||||||
{
|
{
|
||||||
|
if (!this._isIpAllowed(req, this.allowedIps.manage))
|
||||||
|
{
|
||||||
|
this._logRequestWarning(req, 'IP address not in allowedIps.manage');
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.notificationFacade.setReminders(req.params.token, false);
|
await this.notificationFacade.setReminders(req.params.token, false);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}
|
}
|
||||||
@ -69,6 +99,13 @@ class ApiRoutes
|
|||||||
|
|
||||||
async _handleLatchedNotifications(req, res)
|
async _handleLatchedNotifications(req, res)
|
||||||
{
|
{
|
||||||
|
if (!this._isIpAllowed(req, this.allowedIps.manage))
|
||||||
|
{
|
||||||
|
this._logRequestWarning(req, 'IP address not in allowedIps.manage');
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer '))
|
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer '))
|
||||||
{
|
{
|
||||||
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
||||||
@ -90,6 +127,12 @@ class ApiRoutes
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_handleIp(req, res)
|
||||||
|
{
|
||||||
|
res.send(this._getIp(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_wrapAsyncHandler(handler)
|
_wrapAsyncHandler(handler)
|
||||||
{
|
{
|
||||||
const boundHandler = handler.bind(this);
|
const boundHandler = handler.bind(this);
|
||||||
@ -114,6 +157,18 @@ class ApiRoutes
|
|||||||
{
|
{
|
||||||
this.logger.warn(`[${req.ip}] ${message}`);
|
this.logger.warn(`[${req.ip}] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_isIpAllowed(req, whitelist)
|
||||||
|
{
|
||||||
|
return whitelist === null || whitelist.includes(this._getIp(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_getIp(req)
|
||||||
|
{
|
||||||
|
return req.headers['x-forwarded-for'] || req.ip;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user