Implemented reminders
Added IP filters
This commit is contained in:
parent
2a39aabaa0
commit
9e226013dc
@ -2,6 +2,10 @@ module.exports = {
|
||||
port: 3369,
|
||||
publicUrl: 'https://your.domain.name/',
|
||||
authToken: '<token required to push notifications>',
|
||||
allowedIps: {
|
||||
post: ['127.0.0.1'],
|
||||
manage: null
|
||||
},
|
||||
|
||||
dataFilename: './data.json',
|
||||
salt: '<generate a random string of characters>',
|
||||
@ -10,7 +14,8 @@ module.exports = {
|
||||
enabled: true,
|
||||
interval: { days: 1 },
|
||||
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: [
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
notification: {
|
||||
loading: 'Loading, please wait...',
|
||||
tokenInvalid: 'Invalid token',
|
||||
tokenInvalid: 'Invalid token or access denied',
|
||||
listHeader: 'Disabled notifications',
|
||||
|
||||
latchTime: 'Sent on {latchTime}',
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
notification: {
|
||||
loading: 'Bezig met laden, een ogenblik geduld a.u.b....',
|
||||
tokenInvalid: 'Ongeldig token',
|
||||
tokenInvalid: 'Ongeldig token of geen toegang',
|
||||
listHeader: 'Uitgeschakelde meldingen',
|
||||
|
||||
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 TransportProvider = require('./src/transport/provider');
|
||||
const SubjectParser = require('./src/subjectparser');
|
||||
const ReminderScheduler = require('./src/reminderscheduler');
|
||||
|
||||
|
||||
const logger = winston.createLogger({
|
||||
@ -17,7 +18,7 @@ const logger = winston.createLogger({
|
||||
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.simple()
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
const container = new Container();
|
||||
@ -29,6 +30,7 @@ container.registerType('NotificationRepository', NotificationRepository);
|
||||
container.registerType('NotificationFacade', NotificationFacade);
|
||||
container.registerType('TransportProvider', TransportProvider);
|
||||
container.registerType('SubjectParser', SubjectParser);
|
||||
container.registerType('ReminderScheduler', ReminderScheduler);
|
||||
|
||||
|
||||
async function asyncMain()
|
||||
@ -51,6 +53,7 @@ async function asyncMain()
|
||||
|
||||
|
||||
await container.NotificationRepository.init();
|
||||
container.ReminderScheduler.start();
|
||||
|
||||
const app = express();
|
||||
|
||||
@ -60,7 +63,7 @@ async function asyncMain()
|
||||
app.use('/api', container.ApiRoutes.createRouter(express));
|
||||
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}`);
|
||||
});
|
||||
|
@ -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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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.notificationFacade = notificationFacade;
|
||||
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/reminders/enable', this._wrapAsyncHandler(this._handleEnableReminders));
|
||||
router.post('/notification/:token/reminders/disable', this._wrapAsyncHandler(this._handleDisableReminders));
|
||||
router.get('/ip', this._handleIp.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
@ -27,6 +29,13 @@ class ApiRoutes
|
||||
|
||||
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)
|
||||
{
|
||||
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
||||
@ -48,6 +57,13 @@ class ApiRoutes
|
||||
|
||||
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);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
@ -55,6 +71,13 @@ class ApiRoutes
|
||||
|
||||
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);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
@ -62,6 +85,13 @@ class ApiRoutes
|
||||
|
||||
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);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
@ -69,6 +99,13 @@ class ApiRoutes
|
||||
|
||||
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 '))
|
||||
{
|
||||
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
||||
@ -90,6 +127,12 @@ class ApiRoutes
|
||||
}
|
||||
|
||||
|
||||
_handleIp(req, res)
|
||||
{
|
||||
res.send(this._getIp(req));
|
||||
}
|
||||
|
||||
|
||||
_wrapAsyncHandler(handler)
|
||||
{
|
||||
const boundHandler = handler.bind(this);
|
||||
@ -113,7 +156,19 @@ class ApiRoutes
|
||||
_logRequestWarning(req, 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