Implemented reminders

Added IP filters
This commit is contained in:
Mark van Renswoude 2021-08-18 15:50:34 +02:00
parent 2a39aabaa0
commit 9e226013dc
8 changed files with 178 additions and 6 deletions

View File

@ -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: [

View File

@ -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}',

View File

@ -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}',

View File

@ -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}`);
}); });

View File

@ -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));

View File

@ -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
View 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;

View File

@ -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;
}
} }