Implemented email notifications

This commit is contained in:
Mark van Renswoude 2018-04-28 15:00:30 +02:00
parent b17916be4b
commit 0ac43ce0e5
10 changed files with 1394 additions and 113 deletions

View File

@ -22,5 +22,26 @@ module.exports = {
// If a collision occurs, a new code will be generated and tested again for up to 100 times.
alphabet: '1234567890abcdef',
length: 8
},
notifications: {
interval: 10,
maxAttempt: 12,
adminUrl: 'http://localhost:3001/admin/',
mail: {
from: '"Recv" <recv@localhost>',
transport: {
// passed directly to NodeMailer's createTransport. See nodemailer.com for more information.
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: 'recv',
pass: 'test'
}
}
}
}
};

View File

@ -0,0 +1,33 @@
<%
function humanFileSize(bytes, si)
{
var thresh = si ? 1000 : 1024;
if(Math.abs(bytes) < thresh)
{
return bytes + ' B';
}
var units = si
? ['kB','MB','GB','TB','PB','EB','ZB','YB']
: ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
var u = -1;
do
{
bytes /= thresh;
++u;
}
while(Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + ' ' + units[u];
}
%>
<p>Hello <%= user.name %>,</p>
<p>The following files have just been uploaded:</p>
<ul>
<% upload.files.forEach((file) => { %>
<li><%= file.name %> (<%= humanFileSize(file.size, true) %>)</li>
<% }) %>
</ul>
<p>You can download these files by logging in to <a href="<%= adminUrl %>"><%= adminUrl %></a></p>
<p>Cheers,<br />Recv</p>

View File

@ -0,0 +1 @@
📄 File upload notification

View File

@ -2,6 +2,7 @@
const config = require('./config');
const Repository = require('./lib/repository');
const NotificationWorker = require('./lib/workers/notification');
const _ = require('lodash');
const fs = require('fs');
@ -96,6 +97,10 @@ const webpackConfig = require('./webpack.config.js');
app.get('/admin', (req, res) => { res.redirect(301, '/#/admin/') });
// Background workers
var notificationWorker = new NotificationWorker(repository);
notificationWorker.start(config.notifications.interval * 1000);
var server = app.listen(config.port, () => console.log('Recv running on port ' + server.address().port));
}

View File

@ -106,6 +106,11 @@ module.exports = (repository, tusServer) =>
}
var uploadId = await repository.uploads.insert(decoded.codeUserId, decoded.code, req.body.files, expiration);
await repository.notifications.insert({
userId: decoded.codeUserId,
uploadId: uploadId
});
res.send({ id: uploadId });
});
});

View File

@ -7,6 +7,7 @@ const Datastore = require('nedb');
const UserRepository = require('./user').UserRepository;
const CodeRepository = require('./code').CodeRepository;
const UploadRepository = require('./upload').UploadRepository;
const NotificationRepository = require('./notification').NotificationRepository;
class Repository
@ -46,6 +47,7 @@ class Repository
self.users = new UserRepository(initStore('user.db'));
self.codes = new CodeRepository(initStore('code.db'));
self.uploads = new UploadRepository(initStore('upload.db'));
self.notifications = new NotificationRepository(initStore('notification.db'));
self.users.init()
.then(() => { resolve() })

View File

@ -0,0 +1,123 @@
const _ = require('lodash');
class Notification
{
constructor(values)
{
var self = this;
self.id = values.id || values._id || null;
self.userId = values.userId || null;
self.uploadId = values.uploadId || null;
self.attempt = values.attempt || 0;
}
}
class NotificationRepository
{
constructor(store)
{
var self = this;
self.store = store;
}
list()
{
var self = this;
return new Promise((resolve, reject) =>
{
self.store.find({}, (err, docs) =>
{
if (err)
{
reject(err);
return;
}
resolve(_.map(docs, (doc) => new Notification(doc)));
});
});
}
insert(notification)
{
var self = this;
return new Promise((resolve, reject) =>
{
self.store.insert({
userId: notification.userId,
uploadId: notification.uploadId,
attempt: notification.attempt
}, (err, dbNotification) =>
{
if (err)
{
reject(err);
return;
}
resolve(dbNotification._id);
});
});
}
update(notification)
{
var self = this;
return new Promise((resolve, reject) =>
{
self.store.update({ _id: notification.id }, { $set: {
attempt: notification.attempt
}},
(err, numAffected) =>
{
if (err)
{
reject(err);
return;
}
if (numAffected == 0)
{
reject();
}
resolve();
});
});
}
delete(notificationId)
{
var self = this;
return new Promise((resolve, reject) =>
{
self.store.remove({ _id: notificationId }, (err, numRemoved) =>
{
if (err)
{
reject(err);
return;
}
resolve();
});
});
}
}
module.exports = {
Notification,
NotificationRepository
}

123
lib/workers/notification.js Normal file
View File

@ -0,0 +1,123 @@
const config = require('../../config');
const async = require('async');
const nodemailer = require('nodemailer');
const Email = require('email-templates');
class NotificationWorker
{
constructor(repository)
{
this.repository = repository;
}
start(interval)
{
var self = this;
self.stop();
self.timer = setInterval(async () =>
{
if (self.ticking)
return;
self.ticking = true;
await self.tick();
self.ticking = false;
}, interval);
}
stop()
{
var self = this;
if (self.timer)
{
clearInterval(self.timer);
self.timer = null;
}
}
async tick()
{
var self = this;
var notifications = await self.repository.notifications.list();
return new Promise((resolve, reject) =>
{
async.eachOfSeries(notifications,
async (item) => { await self.sendNotification(item) },
(err) =>
{
if (err)
reject(err);
else
resolve();
});
})
}
async sendNotification(notification)
{
let self = this;
let user = await self.repository.users.get(notification.userId);
if (user === null || !user.email)
return;
let upload = await self.repository.uploads.get(notification.uploadId);
if (upload === null)
return;
return new Promise((resolve, reject) =>
{
const email = new Email({
message: {
from: config.notifications.mail.from
},
send: true,
preview: false,
transport: nodemailer.createTransport(config.notifications.mail.transport),
views: {
options: {
extension: 'ejs'
}
}
});
email
.send({
template: 'uploadnotification',
message: {
to: user.email
},
locals: {
user: user,
upload: upload,
adminUrl: config.notifications.adminUrl
}
})
.then(async () =>
{
await self.repository.notifications.delete(notification.id);
console.log('Notification sent to: ' + user.email + ' for upload ID: ' + upload.id);
resolve();
})
.catch(async (error) =>
{
notification.attempt++;
if (notification.attempt > config.notifications.maxAttempts)
await self.repository.notifications.delete(notification.id);
else
await self.repository.notifications.update(notification);
resolve();
});
});
}
}
module.exports = NotificationWorker;

1190
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,8 @@
"cookie-parser": "^1.4.3",
"debug": "^3.1.0",
"diskusage": "^0.2.4",
"ejs": "^2.5.9",
"email-templates": "^3.6.0",
"express": "^4.16.3",
"express-async-handler": "^1.1.2",
"js-cookie": "^2.2.0",
@ -36,7 +38,9 @@
"mz": "^2.7.0",
"nanoid": "^1.0.2",
"nedb": "^1.8.0",
"nodemailer": "^4.6.4",
"npm": "^5.8.0",
"pug": "^2.0.3",
"resolve-path": "^1.4.0",
"tus-node-server": "^0.2.10"
},