diff --git a/emails/movednotification/html.ejs b/emails/movednotification/html.ejs new file mode 100644 index 0000000..73497e8 --- /dev/null +++ b/emails/movednotification/html.ejs @@ -0,0 +1,20 @@ +<% let expirationDate = null; %> +

Hello <%=user.name%>,

+

The following files have been assigned from <%=prevUser.name%> to you<%=prevUser.userId !== assignUser.userId ? ' by ' + assignUser.name : ''%>:

+ +<% if (expirationDate !== null) { %> +These files will be automatically deleted after <%=expirationDate.toLocaleString('en-US')%> +<% } %> +

You can download these files by logging in to <%=adminUrl%>

+

Cheers,
Recv

\ No newline at end of file diff --git a/emails/movednotification/subject.ejs b/emails/movednotification/subject.ejs new file mode 100644 index 0000000..af15336 --- /dev/null +++ b/emails/movednotification/subject.ejs @@ -0,0 +1 @@ +📄 <%=fileCount%> file<%=fileCount != 1 ? 's have' : ' has'%> been assigned to you \ No newline at end of file diff --git a/emails/uploadnotification/html.ejs b/emails/uploadnotification/html.ejs index 7711bf7..550c0e2 100644 --- a/emails/uploadnotification/html.ejs +++ b/emails/uploadnotification/html.ejs @@ -1,36 +1,12 @@ -<% -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]; -} -%> -

Hello <%= user.name %>,

-

The following files have just been uploaded:

+

Hello <%=user.name%>,

+

The following files have been uploaded:

<% if (upload.expirationDate !== null) { %> These files will be automatically deleted after <%=upload.expirationDate.toLocaleString('en-US')%> <% } %> -

You can download these files by logging in to <%= adminUrl %>

+

You can download these files by logging in to <%=adminUrl%>

Cheers,
Recv

\ No newline at end of file diff --git a/emails/uploadnotification/subject.ejs b/emails/uploadnotification/subject.ejs index 32d1674..e790395 100644 --- a/emails/uploadnotification/subject.ejs +++ b/emails/uploadnotification/subject.ejs @@ -1 +1 @@ -📄 File upload notification \ No newline at end of file +📄 <%=upload.files.count%> file<%=upload.files.count != 1 ? 's' : ''%> uploaded \ No newline at end of file diff --git a/index.js b/index.js index cb92fd1..15faaeb 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ 'use strict' const fs = require('fs'); -const merge = require('deepmerge'); +const _ = require('lodash'); let configDefaults = require('./config.defaults'); if (fs.existsSync('./config.js')) { let configChanges = require('./config'); - global.config = merge(configDefaults, configChanges); + global.config = _.merge(configDefaults, configChanges); } else global.config = configDefaults; diff --git a/lib/api/admin.js b/lib/api/admin.js deleted file mode 100644 index 9686485..0000000 --- a/lib/api/admin.js +++ /dev/null @@ -1,387 +0,0 @@ -const express = require('express'); -const asyncHandler = require('express-async-handler'); -const jwt = require('jsonwebtoken'); -const path = require('path'); -const resolvePath = require('resolve-path'); -const AuthTokens = require('../authtokens'); -const ExpirationUnits = require('../expirationunits'); -const disk = require('diskusage'); -const fs = require('mz/fs'); - - -async function checkAuthorization(req, res, repository, onVerified) -{ - var token; - - if (req.headers.authorization) - { - if (req.headers.authorization.split(' ')[0] !== 'Bearer') - { - res.sendStatus(400); - return; - } - - token = req.headers.authorization.split(' ')[1]; - } - else if (req.cookies && req.cookies.adminToken) - { - token = req.cookies.adminToken; - } - else - { - res.sendStatus(403); - return; - } - - - jwt.verify(token, config.jwtSecret, async (err, decoded) => - { - try - { - if (err) - { - res.sendStatus(403); - return; - } - - if (decoded.userId) - { - var user = await repository.users.get(decoded.userId); - if (user === null || !user.active) - { - res.sendStatus(403); - return; - } - else - await onVerified(user); - } - else - res.sendStatus(400); - } - catch (e) - { - console.log(e); - res.sendStatus(500); - } - }); -} - - - -module.exports = (repository) => -{ - var router = express.Router(); - - - router.get('/whoami', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - res.send({ - userId: user.id, - username: user.username, - auth: user.auth - }); - }); - })); - - - router.get('/diskspace', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - disk.check(config.fileUpload.path, (err, info) => - { - if (err) - { - res.sendStatus(500); - return; - } - - res.send(info); - }); - }); - })); - - - /* - Codes - */ - router.get('/codes', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var codes = await repository.codes.list(user.hasAuth(AuthTokens.ViewAllCodes) ? null : user.id); - var usernames = await repository.users.getNames(); - - codes.forEach((item) => - { - item.username = usernames[item.userId]; - }); - - res.send(codes); - }); - })); - - - router.get('/codes/:id', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var code = await repository.codes.get(req.params.id); - if (code === null || (code.userId !== user.id && !user.hasAuth(AuthTokens.ViewAllCodes))) - { - res.sendStatus(404); - return; - } - - var user = await repository.users.get(code.userId); - if (user !== null) - code.username = user.name; - - res.send(code); - }); - })); - - - router.post('/codes', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var postedCode = req.body; - - - if (config.code.maxExpiration !== null) - { - let now = new Date(); - - if (ExpirationUnits.apply(postedCode.expiration) > ExpirationUnits.apply(config.code.maxExpiration)) - { - res.sendStatus(400); - return; - } - } - - - if (postedCode.id) - { - var code = await repository.codes.get(postedCode.id); - if (code === null || (code.userId !== user.id && !user.hasAuth(AuthTokens.ViewAllCodes))) - { - res.sendStatus(404); - return; - } - - await repository.codes.update({ - id: postedCode.id, - expiration: postedCode.expiration, - description: postedCode.description, - message: postedCode.message - }); - - res.sendStatus(200); - } - else - { - var codeId = await repository.codes.insert({ - userId: user.id, - created: postedCode.created || new Date(), - expiration: postedCode.expiration, - description: postedCode.description, - message: postedCode.message - }); - } - - res.send(codeId); - }); - })); - - - router.delete('/codes/:id', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var code = await repository.codes.get(req.params.id); - if (code == null || (code.userId !== user.id && !user.hasAuth(AuthTokens.ViewAllCodes))) - { - res.sendStatus(404); - return; - } - - repository.codes.delete(code.id); - res.sendStatus(200); - }); - })); - - - router.get('/expiration', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - res.send({ - max: config.code.maxExpiration, - default: config.code.defaultExpiration - }); - }); - })); - - /* - Uploads - */ - router.get('/uploads', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var files = await repository.uploads.list(user.hasAuth(AuthTokens.ViewAllUploads) ? null : user.id); - var usernames = await repository.users.getNames(); - var codedescriptions = await repository.codes.getDescriptions(); - - files.forEach((item) => - { - item.username = item.userId !== null ? usernames[item.userId] : null; - item.codedescription = item.code !== null ? codedescriptions[item.code] : null; - }); - - res.send(files); - }); - })); - - - router.delete('/uploads/:id', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var upload = await repository.uploads.get(req.params.id); - if (upload == null || (upload.userId !== user.id && !user.hasAuth(AuthTokens.ViewAllUploads))) - { - res.sendStatus(404); - return; - } - - await repository.uploads.delete(upload.id); - res.sendStatus(200); - }); - })); - - - router.get('/download/:fileid/:displayname', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - // TODO should we check if the user has access to the file? - // for now not that important, if you know the file's UID and are logged in - - var fullpath = resolvePath(config.fileUpload.path, req.params.fileid); - res.download(fullpath, req.params.displayname); - }); - })); - - - /* - Users - */ - router.get('/users', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - if (!user.hasAuth(AuthTokens.ManageUsers)) - { - res.sendStatus(403); - return; - } - - var users = await repository.users.list(); - res.send(users); - }); - })); - - - router.get('/users/:id', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - if (req.params.id !== user.id && !user.hasAuth(AuthTokens.ManageUsers)) - { - res.sendStatus(404); - return; - } - - var user = await repository.users.get(req.params.id); - if (user === null) - { - res.sendStatus(404); - return; - } - - res.send(user); - }); - })); - - - router.post('/users', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - var postedUser = req.body; - - if (postedUser.id) - { - if (postedUser.id !== user.id && !user.hasAuth(AuthTokens.ManageUsers)) - { - res.sendStatus(403); - return; - } - - await repository.users.update({ - id: postedUser.id, - username: postedUser.username, - name: postedUser.name, - password: postedUser.password, - email: postedUser.email, - auth: postedUser.auth, - active: postedUser.active - }); - - res.sendStatus(200); - } - else - { - if (!user.hasAuth(AuthTokens.ManageUsers)) - { - res.sendStatus(403); - return; - } - - var userId = await repository.users.insert({ - username: postedUser.username, - name: postedUser.name, - password: postedUser.password, - email: postedUser.email, - auth: postedUser.auth, - active: postedUser.active, - createdByUserId: postedUser.createdByUserId - }); - } - - res.send(userId); - }); - })); - - - router.delete('/users/:id', asyncHandler(async (req, res) => - { - await checkAuthorization(req, res, repository, async (user) => - { - if (!user.hasAuth(AuthTokens.ManageUsers)) - { - res.sendStatus(403); - return; - } - - repository.users.delete(req.params.id); - res.sendStatus(200); - }); - })); - - return router; -} \ No newline at end of file diff --git a/lib/api/admin/codes.js b/lib/api/admin/codes.js new file mode 100644 index 0000000..8e3a12a --- /dev/null +++ b/lib/api/admin/codes.js @@ -0,0 +1,171 @@ +const asyncHandler = require('express-async-handler'); +const AuthTokens = require('../../authtokens'); +const ExpirationUnits = require('../../expirationunits'); +const NotificationType = require('../../repository/notification').NotificationType; +const _ = require('lodash'); + + +module.exports = (repository, router) => +{ + router.get('/codes', asyncHandler(async (req, res) => + { + var codes = await repository.codes.list(req.user.hasAuth(AuthTokens.ViewAllCodes) ? null : req.user.id); + var usernames = await repository.users.getNames(); + + codes.forEach((item) => + { + item.username = usernames[item.userId]; + }); + + res.send(codes); + })); + + + router.get('/codes/:id', asyncHandler(async (req, res) => + { + var code = await repository.codes.get(req.params.id); + if (code === null || (code.userId !== req.user.id && !req.user.hasAuth(AuthTokens.ViewAllCodes))) + { + res.sendStatus(404); + return; + } + + var user = await repository.users.get(code.userId); + if (user !== null) + code.username = user.name; + + res.send(code); + })); + + + router.post('/codes', asyncHandler(async (req, res) => + { + var postedCode = req.body; + + + if (config.code.maxExpiration !== null) + { + let now = new Date(); + + if (ExpirationUnits.apply(postedCode.expiration) > ExpirationUnits.apply(config.code.maxExpiration)) + { + res.sendStatus(400); + return; + } + } + + + if (postedCode.id) + { + var code = await repository.codes.get(postedCode.id); + if (code === null || (code.userId !== req.user.id && !req.user.hasAuth(AuthTokens.ViewAllCodes))) + { + res.sendStatus(404); + return; + } + + await repository.codes.update({ + id: postedCode.id, + expiration: postedCode.expiration, + description: postedCode.description, + message: postedCode.message + }); + + res.sendStatus(200); + } + else + { + var codeId = await repository.codes.insert({ + userId: req.user.id, + created: postedCode.created || new Date(), + expiration: postedCode.expiration, + description: postedCode.description, + message: postedCode.message + }); + } + + res.send(codeId); + })); + + + router.delete('/codes/:id', asyncHandler(async (req, res) => + { + var code = await repository.codes.get(req.params.id); + if (code == null || (code.userId !== req.user.id && !req.user.hasAuth(AuthTokens.ViewAllCodes))) + { + res.sendStatus(404); + return; + } + + repository.codes.delete(code.id); + res.sendStatus(200); + })); + + + router.get('/expiration', asyncHandler(async (req, res) => + { + res.send({ + max: config.code.maxExpiration, + default: config.code.defaultExpiration + }); + })); + + + router.post('/assign/code', asyncHandler(async (req, res) => + { + var postedCode = req.body; + + var code = await repository.codes.get(postedCode.id); + if (code === null || (code.userId !== req.user.id && !req.user.hasAuth(AuthTokens.ViewAllCodes))) + { + res.sendStatus(404); + return; + } + + if (code.userId !== postedCode.userId) + { + var target = await repository.users.get(postedCode.userId); + if (target === null) + { + res.sendStatus(400); + return; + } + + await repository.codes.move(postedCode.id, postedCode.userId); + await repository.uploads.move(postedCode.id, postedCode.userId); + + await repository.notifications.insert({ + userId: postedCode.userId, + codeId: postedCode.id, + type: NotificationType.CodeMoved, + metadata: { + prevUserId: code.userId, + assignUserId: req.user.id + } + }); + } + + res.sendStatus(200); + })); + + + router.get('/assign/users', asyncHandler(async (req, res) => + { + var users = await repository.users.list(); + if (users === null) + { + res.send([]); + return; + } + + let assignableUsers = _.map(_.filter(users, + (user) => { return user.active }), + (user) => { return { + id: user.id, + username: user.username, + name: user.name + }} ); + + res.send(assignableUsers); + })); +} \ No newline at end of file diff --git a/lib/api/admin/index.js b/lib/api/admin/index.js new file mode 100644 index 0000000..5f78207 --- /dev/null +++ b/lib/api/admin/index.js @@ -0,0 +1,88 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); + + +async function checkAuthorization(req, res, repository, onVerified) +{ + var token; + + if (req.headers.authorization) + { + if (req.headers.authorization.split(' ')[0] !== 'Bearer') + { + res.sendStatus(400); + return; + } + + token = req.headers.authorization.split(' ')[1]; + } + else if (req.cookies && req.cookies.adminToken) + { + token = req.cookies.adminToken; + } + else + { + res.sendStatus(403); + return; + } + + + jwt.verify(token, config.jwtSecret, async (err, decoded) => + { + try + { + if (err) + { + res.sendStatus(403); + return; + } + + if (decoded.userId) + { + var user = await repository.users.get(decoded.userId); + if (user === null || !user.active) + { + res.sendStatus(403); + return; + } + else + await onVerified(user); + } + else + res.sendStatus(400); + } + catch (e) + { + console.log(e); + res.sendStatus(500); + } + }); +} + + +module.exports = (repository) => +{ + var router = express.Router(); + router.use(async (req, res, next) => + { + try + { + await checkAuthorization(req, res, repository, (user) => + { + req.user = user; + next(); + }); + } + catch (err) + { + console.log(err); + } + }); + + require('./status')(repository, router); + require('./codes')(repository, router); + require('./uploads')(repository, router); + require('./users')(repository, router); + + return router; +} \ No newline at end of file diff --git a/lib/api/admin/status.js b/lib/api/admin/status.js new file mode 100644 index 0000000..e7cb93f --- /dev/null +++ b/lib/api/admin/status.js @@ -0,0 +1,30 @@ +const asyncHandler = require('express-async-handler'); +const disk = require('diskusage'); + + +module.exports = (repository, router) => +{ + router.get('/whoami', (req, res) => + { + res.send({ + userId: req.user.id, + username: req.user.username, + auth: req.user.auth + }); + }); + + + router.get('/diskspace', (req, res) => + { + disk.check(config.fileUpload.path, (err, info) => + { + if (err) + { + res.sendStatus(500); + return; + } + + res.send(info); + }); + }); +} \ No newline at end of file diff --git a/lib/api/admin/uploads.js b/lib/api/admin/uploads.js new file mode 100644 index 0000000..7fd3739 --- /dev/null +++ b/lib/api/admin/uploads.js @@ -0,0 +1,74 @@ +const asyncHandler = require('express-async-handler'); +const AuthTokens = require('../../authtokens'); +const resolvePath = require('resolve-path'); + + +module.exports = (repository, router) => +{ + router.get('/uploads', asyncHandler(async (req, res) => + { + var files = await repository.uploads.list(req.user.hasAuth(AuthTokens.ViewAllUploads) ? null : req.user.id); + var usernames = await repository.users.getNames(); + var codedescriptions = await repository.codes.getDescriptions(); + + files.forEach((item) => + { + item.username = item.userId !== null ? usernames[item.userId] : null; + item.codedescription = item.codeId !== null ? codedescriptions[item.codeId] : null; + }); + + res.send(files); + })); + + + router.delete('/uploads/:id', asyncHandler(async (req, res) => + { + var upload = await repository.uploads.get(req.params.id); + if (upload == null || (upload.userId !== req.user.id && !req.user.hasAuth(AuthTokens.ViewAllUploads))) + { + res.sendStatus(404); + return; + } + + await repository.uploads.delete(upload.id); + res.sendStatus(200); + })); + + + router.delete('/codeuploads/:code', asyncHandler(async (req, res) => + { + var uploads = await repository.uploads.listForCode(req.params.code); + if (uploads === null) + { + res.sendStatus(404); + return; + } + + if (!req.user.hasAuth(AuthTokens.ViewAllUploads)) + { + for (let i = 0; i < uploads.length; i++) + { + if (uploads[i].userId !== req.user.id) + { + res.sendStatus(404); + return; + } + } + } + + for (let i = 0; i < uploads.length; i++) + await repository.uploads.delete(uploads[i].id); + + res.sendStatus(200); + })); + + + router.get('/download/:fileid/:displayname', asyncHandler(async (req, res) => + { + // TODO should we check if the user has access to the file? + // for now not that important, if you know the file's UID and are logged in + + var fullpath = resolvePath(config.fileUpload.path, req.params.fileid); + res.download(fullpath, req.params.displayname); + })); +} \ No newline at end of file diff --git a/lib/api/admin/users.js b/lib/api/admin/users.js new file mode 100644 index 0000000..5ba1da6 --- /dev/null +++ b/lib/api/admin/users.js @@ -0,0 +1,97 @@ +const asyncHandler = require('express-async-handler'); +const AuthTokens = require('../../authtokens'); + + +module.exports = (repository, router) => +{ + router.get('/users', asyncHandler(async (req, res) => + { + if (!req.user.hasAuth(AuthTokens.ManageUsers)) + { + res.sendStatus(403); + return; + } + + var users = await repository.users.list(); + res.send(users); + })); + + + router.get('/users/:id', asyncHandler(async (req, res) => + { + if (req.params.id !== req.user.id && !req.user.hasAuth(AuthTokens.ManageUsers)) + { + res.sendStatus(404); + return; + } + + var user = await repository.users.get(req.params.id); + if (user === null) + { + res.sendStatus(404); + return; + } + + res.send(user); + })); + + + router.post('/users', asyncHandler(async (req, res) => + { + var postedUser = req.body; + + if (postedUser.id) + { + if (postedUser.id !== req.user.id && !req.user.hasAuth(AuthTokens.ManageUsers)) + { + res.sendStatus(403); + return; + } + + await repository.users.update({ + id: postedUser.id, + username: postedUser.username, + name: postedUser.name, + password: postedUser.password, + email: postedUser.email, + auth: postedUser.auth, + active: postedUser.active + }); + + res.sendStatus(200); + } + else + { + if (!req.user.hasAuth(AuthTokens.ManageUsers)) + { + res.sendStatus(403); + return; + } + + var userId = await repository.users.insert({ + username: postedUser.username, + name: postedUser.name, + password: postedUser.password, + email: postedUser.email, + auth: postedUser.auth, + active: postedUser.active, + createdByUserId: postedUser.createdByUserId + }); + } + + res.send(userId); + })); + + + router.delete('/users/:id', asyncHandler(async (req, res) => + { + if (!req.user.hasAuth(AuthTokens.ManageUsers)) + { + res.sendStatus(403); + return; + } + + repository.users.delete(req.params.id); + res.sendStatus(200); + })); +} \ No newline at end of file diff --git a/lib/api/token.js b/lib/api/token.js index 4a90b48..68d6982 100644 --- a/lib/api/token.js +++ b/lib/api/token.js @@ -19,7 +19,7 @@ module.exports = (repository) => if (code !== null) { jwt.sign({ - code: req.body.code, + codeId: req.body.code, codeUserId: code.userId, codeExpirationTime: code.expirationDate !== null ? code.expirationDate.getTime() : null }, config.jwtSecret, (err, token) => diff --git a/lib/api/upload.js b/lib/api/upload.js index 9ea6cd0..330f7a1 100644 --- a/lib/api/upload.js +++ b/lib/api/upload.js @@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken'); const resolvePath = require('resolve-path'); const fs = require('fs'); const async = require('async'); +const NotificationType = require('../repository/notification').NotificationType; async function checkAuthorization(req, res, onVerified) @@ -25,7 +26,7 @@ async function checkAuthorization(req, res, onVerified) return; } - if (decoded.code) + if (decoded.codeId) await onVerified(decoded); else res.sendStatus(400); @@ -106,16 +107,17 @@ module.exports = (repository, tusServer) => return; } - var uploadId = await repository.uploads.insert( - decoded.codeUserId, - decoded.code, - req.body.files, - decoded.codeExpirationTime !== null ? new Date(decoded.codeExpirationTime) : null - ); + var uploadId = await repository.uploads.insert({ + userId: decoded.codeUserId, + codeId: decoded.codeId, + files: req.body.files, + expirationDate: decoded.codeExpirationTime !== null ? new Date(decoded.codeExpirationTime) : null + }); await repository.notifications.insert({ userId: decoded.codeUserId, - uploadId: uploadId + uploadId: uploadId, + type: NotificationType.UploadComplete }); res.send({ id: uploadId }); diff --git a/lib/repository/code.js b/lib/repository/code.js index 3c2c723..f06962e 100644 --- a/lib/repository/code.js +++ b/lib/repository/code.js @@ -20,6 +20,10 @@ class Code self.description = values.description || null; self.message = values.message || null; self.messageHTML = values.messageHTML || null; + self.history = values.history || [{ + userId: self.userId, + date: self.created + }]; } } @@ -55,15 +59,23 @@ class CodeRepository if ((await self.get(codeId)) !== null) throw new Error('Code ' + codeId + ' already exists'); + let now = new Date(); + self.store.insert({ _id: codeId, userId: code.userId, - created: code.created || new Date(), + created: code.created || now, expiration: code.expiration, expirationDate: ExpirationUnits.apply(code.expiration, code.created), description: code.description, message: code.message, - messageHTML: self.getMessageHTML(code.message) + messageHTML: self.getMessageHTML(code.message), + history: [ + { + userId: code.userId, + date: code.created || now + } + ] }) return codeId; @@ -107,6 +119,42 @@ class CodeRepository } + move(codeId, userId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.update({ _id: codeId }, { + $set: { + userId: userId + }, + $push: { + history: { + userId: userId, + date: new Date() + } + } + }, + (err, numAffected) => + { + if (err) + { + reject(err); + return; + } + + if (numAffected == 0) + { + reject(); + } + + resolve(); + }); + }); + } + + list(userId) { var self = this; @@ -180,13 +228,13 @@ class CodeRepository } - delete(code) + delete(codeId) { var self = this; return new Promise((resolve, reject) => { - self.store.remove({ _id: code }, (err, numRemoved) => + self.store.remove({ _id: codeId }, (err, numRemoved) => { if (err) { diff --git a/lib/repository/notification.js b/lib/repository/notification.js index 7531c97..c87798e 100644 --- a/lib/repository/notification.js +++ b/lib/repository/notification.js @@ -1,6 +1,12 @@ const map = require('lodash/map'); +const NotificationType = { + UploadComplete: 'uploadComplete', + CodeMoved: 'codeMoved' +} + + class Notification { constructor(values) @@ -8,9 +14,12 @@ class Notification var self = this; self.id = values.id || values._id || null; - self.userId = values.userId || null; self.uploadId = values.uploadId || null; + self.codeId = values.codeId || null; + self.userId = values.userId || null; + self.type = values.type || NotificationType.UploadComplete; self.attempt = values.attempt || 0; + self.metadata = values.metadata || null; } } @@ -52,8 +61,11 @@ class NotificationRepository { self.store.insert({ userId: notification.userId, + codeId: notification.codeId, uploadId: notification.uploadId, - attempt: notification.attempt + attempt: notification.attempt, + type: notification.type, + metadata: notification.metadata }, (err, dbNotification) => { if (err) @@ -118,6 +130,7 @@ class NotificationRepository module.exports = { + NotificationType, Notification, NotificationRepository } \ No newline at end of file diff --git a/lib/repository/upload.js b/lib/repository/upload.js index 798c09e..105de5d 100644 --- a/lib/repository/upload.js +++ b/lib/repository/upload.js @@ -13,7 +13,7 @@ class Upload self.id = values.id || values._id || null; self.userId = values.userId || null; - self.code = values.code || null; + self.codeId = values.codeId || null; self.created = values.created || new Date(); self.expirationDate = values.expirationDate || null; self.files = values.files || []; @@ -54,25 +54,25 @@ class UploadRepository } - insert(userId, code, files, expirationDate) + insert(upload) { var self = this; return new Promise((resolve, reject) => { - var upload = { - created: new Date(), - userId: userId, - code: code, - expirationDate: expirationDate, - files: map(filter(files, + let insertUpload = { + created: upload.created || new Date(), + userId: upload.userId, + codeId: upload.codeId, + expirationDate: upload.expirationDate, + files: map(filter(upload.files, (file) => file.hasOwnProperty('id') && file.hasOwnProperty('name')), (file) => { return { id: file.id, name: file.name, size: file.size } }) }; - if (upload.files.length) + if (insertUpload.files.length) { - self.store.insert(upload, (err, dbUpload) => + self.store.insert(insertUpload, (err, dbUpload) => { if (err) { @@ -114,6 +114,29 @@ class UploadRepository } + listForCode(codeId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.find({ codeId: codeId }, (err, docs) => + { + if (err) + { + reject(err); + return; + } + + resolve(docs.map((dbUpload) => + { + return new Upload(dbUpload); + })); + }); + }); + } + + get(uploadId) { var self = this; @@ -134,6 +157,36 @@ class UploadRepository } + move(codeId, userId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.update({ codeId: codeId }, { + $set: { + userId: userId + } + }, + (err, numAffected) => + { + if (err) + { + reject(err); + return; + } + + if (numAffected == 0) + { + reject(); + } + + resolve(); + }); + }); + } + + delete(uploadId) { var self = this; diff --git a/lib/workers/notification.js b/lib/workers/notification.js index 071d73e..8710c4e 100644 --- a/lib/workers/notification.js +++ b/lib/workers/notification.js @@ -5,6 +5,32 @@ const fs = require('mz/fs'); const path = require('path'); const getPaths = require('get-paths'); const AbstractIntervalWorker = require('./abstractintervalworker'); +const NotificationType = require('../repository/notification').NotificationType; +const _ = require('lodash'); + + +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]; +} class NotificationWorker extends AbstractIntervalWorker @@ -24,7 +50,7 @@ class NotificationWorker extends AbstractIntervalWorker return new Promise((resolve, reject) => { async.eachOfSeries(notifications, - async (item) => { await self.sendNotification(item) }, + async (item) => { await self.handleNotification(item) }, (err) => { if (err) @@ -36,17 +62,104 @@ class NotificationWorker extends AbstractIntervalWorker } - async sendNotification(notification) + async handleNotification(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) + switch (notification.type) + { + case NotificationType.UploadComplete: + await self.sendUploadNotification(notification, user); + break; + + case NotificationType.CodeMoved: + await self.sendCodeMovedNotification(notification, user); + break; + } + } + + + getDefaultLocals(user) + { + return { + // Data + user: user, + + // Configuration + adminUrl: config.notifications.adminUrl, + + // Helper functions + humanFileSize: humanFileSize + } + } + + + async sendUploadNotification(notification, user) + { + let self = this; + let locals = self.getDefaultLocals(user); + + locals.upload = await self.repository.uploads.get(notification.uploadId); + if (locals.upload === null) return; + await self.sendNotification(notification, user, locals, 'uploadnotification'); + } + + + async sendCodeMovedNotification(notification, user) + { + let self = this; + let locals = self.getDefaultLocals(user); + + locals.uploads = await self.repository.uploads.listForCode(notification.codeId); + if (locals.uploads === null || locals.uploads.length == 0) + return; + + locals.fileCount = 0; + _.forEach(locals.uploads, (upload) => + { + locals.fileCount += upload.files.length; + }); + + locals.prevUser = { + userId: null, + name: '' + }; + + if (notification.metadata !== null) + { + let resolveUser = async (userId) => + { + if (!userId) + return null; + + let user = await self.repository.users.get(userId); + if (user !== null) + { + return { + userId: userId, + name: user.name + } + } + }; + + locals.prevUser = await resolveUser(notification.metadata.prevUserId); + locals.assignUser = await resolveUser(notification.metadata.assignUserId); + } + + await self.sendNotification(notification, user, locals, 'movednotification'); + } + + + async sendNotification(notification, user, locals, template) + { + let self = this; + return new Promise((resolve, reject) => { const email = new Email({ @@ -99,24 +212,21 @@ class NotificationWorker extends AbstractIntervalWorker email .send({ - template: 'uploadnotification', + template: template, message: { to: user.email }, - locals: { - user: user, - upload: upload, - adminUrl: config.notifications.adminUrl - } + locals: locals }) .then(async () => { await self.repository.notifications.delete(notification.id); - console.log('Notification sent to: ' + user.email + ' for upload ID: ' + upload.id); + console.log('Notification sent to: ' + user.email); resolve(); }) .catch(async (error) => { + console.log(error); notification.attempt++; if (notification.attempt > config.notifications.maxAttempts) await self.repository.notifications.delete(notification.id); diff --git a/package-lock.json b/package-lock.json index 3557edf..c70434e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3132,11 +3132,6 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" }, - "deepmerge": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.0.tgz", - "integrity": "sha512-Q89Z26KAfA3lpPGhbF6XMfYAm3jIV3avViy6KOJ2JLzFbeWHOvPQUu5aSJIWXap3gDZC2y1eF5HXEPI2wGqgvw==" - }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", diff --git a/package.json b/package.json index bbdcc87..4c3b7e8 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "body-parser": "^1.18.2", "cookie-parser": "^1.4.3", "debug": "^3.1.0", - "deepmerge": "^2.1.0", "diskusage": "^0.2.4", "ejs": "^2.5.9", "email-templates": "^3.6.0", diff --git a/public/src/App.vue b/public/src/App.vue index fde4cbe..0cc1621 100644 --- a/public/src/App.vue +++ b/public/src/App.vue @@ -191,6 +191,6 @@ a .confirmDelete { - color: red; + color: red !important; } \ No newline at end of file diff --git a/public/src/app.js b/public/src/app.js index 4026f3c..e6ae335 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -16,6 +16,8 @@ import merge from 'lodash/merge'; import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner'; import faBan from '@fortawesome/fontawesome-free-solid/faBan'; import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; +import faUser from '@fortawesome/fontawesome-free-solid/faUser'; + if (typeof customMessages !== 'undefined') { @@ -32,7 +34,7 @@ if (typeof customMessages !== 'undefined') } -fontawesome.library.add(faSpinner, faBan, faTrashAlt); +fontawesome.library.add(faSpinner, faBan, faTrashAlt, faUser); Vue.component('fa', FontAwesomeIcon); Vue.use(VueI18n); diff --git a/public/src/locale/en.js b/public/src/locale/en.js index d0ee050..542540d 100644 --- a/public/src/locale/en.js +++ b/public/src/locale/en.js @@ -67,7 +67,10 @@ export default { created: 'Date', code: 'Code', owner: 'Owner', - userDeleted: 'deleted' + userDeleted: 'deleted', + + assign: 'Change owner', + assignApply: 'Apply' }, codes: { diff --git a/public/src/locale/nl.js b/public/src/locale/nl.js index 874386a..8fedc40 100644 --- a/public/src/locale/nl.js +++ b/public/src/locale/nl.js @@ -67,8 +67,11 @@ export default { created: 'Datum', code: 'Code', owner: 'Eigenaar', - userDeleted: 'verwijderd' - }, + userDeleted: 'verwijderd', + + assign: 'Verander eigenaar', + assignApply: 'Toepassen' + }, codes: { add: 'Genereer code', diff --git a/public/src/route/admin/Uploads.vue b/public/src/route/admin/Uploads.vue index eac6389..5acb7c0 100644 --- a/public/src/route/admin/Uploads.vue +++ b/public/src/route/admin/Uploads.vue @@ -1,34 +1,46 @@