Implemented changing code ownership

Refactored admin API
This commit is contained in:
Mark van Renswoude 2018-05-13 09:33:10 +02:00
parent 88beaa6f7d
commit d6f553e99f
24 changed files with 919 additions and 497 deletions

View File

@ -0,0 +1,20 @@
<% let expirationDate = null; %>
<p>Hello <%=user.name%>,</p>
<p>The following files have been assigned from <%=prevUser.name%> to you<%=prevUser.userId !== assignUser.userId ? ' by ' + assignUser.name : ''%>:</p>
<ul>
<% uploads.forEach((upload) => { %>
<% upload.files.forEach((file) => { %>
<li><%=file.name%> (<%=humanFileSize(file.size, true)%>)</li>
<%
});
if (upload.expirationDate !== null && (expirationDate === null || upload.expirationDate < expirationDate))
expirationDate = upload.expirationDate;
%>
<% }) %>
</ul>
<% if (expirationDate !== null) { %>
These files will be automatically deleted after <%=expirationDate.toLocaleString('en-US')%>
<% } %>
<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 @@
📄 <%=fileCount%> file<%=fileCount != 1 ? 's have' : ' has'%> been assigned to you

View File

@ -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];
}
%>
<p>Hello <%= user.name %>,</p>
<p>The following files have just been uploaded:</p>
<p>Hello <%=user.name%>,</p>
<p>The following files have been uploaded:</p>
<ul>
<% upload.files.forEach((file) => { %>
<li><%= file.name %> (<%= humanFileSize(file.size, true) %>)</li>
<li><%=file.name%> (<%=humanFileSize(file.size, true)%>)</li>
<% }) %>
</ul>
<% if (upload.expirationDate !== null) { %>
These files will be automatically deleted after <%=upload.expirationDate.toLocaleString('en-US')%>
<% } %>
<p>You can download these files by logging in to <a href="<%= adminUrl %>"><%= adminUrl %></a></p>
<p>You can download these files by logging in to <a href="<%=adminUrl%>"><%=adminUrl%></a></p>
<p>Cheers,<br />Recv</p>

View File

@ -1 +1 @@
📄 File upload notification
📄 <%=upload.files.count%> file<%=upload.files.count != 1 ? 's' : ''%> uploaded

View File

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

View File

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

171
lib/api/admin/codes.js Normal file
View File

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

88
lib/api/admin/index.js Normal file
View File

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

30
lib/api/admin/status.js Normal file
View File

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

74
lib/api/admin/uploads.js Normal file
View File

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

97
lib/api/admin/users.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -191,6 +191,6 @@ a
.confirmDelete
{
color: red;
color: red !important;
}
</style>

View File

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

View File

@ -67,7 +67,10 @@ export default {
created: 'Date',
code: 'Code',
owner: 'Owner',
userDeleted: 'deleted'
userDeleted: 'deleted',
assign: 'Change owner',
assignApply: 'Apply'
},
codes: {

View File

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

View File

@ -1,34 +1,46 @@
<template>
<div id="uploads">
<div v-if="uploads !== null" class="list">
<div v-for="upload in uploads">
<div class="properties">
<div class="pure-g">
<div class="pure-u-1-1"><span class="text codedescription">{{ upload.codedescription || upload.code }}</span></div>
<div class="pure-u-1-3"><span class="text">{{ upload.code }}</span></div>
<div class="pure-u-1-3">
<span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !upload.username }">
{{ upload.username || $t('admin.uploads.userDeleted') }}
</span>
<div v-for="group in groupedUploads">
<template v-for="(upload, index) in group.uploads">
<div class="properties" v-if="index == 0">
<div class="pure-g">
<div class="pure-u-1-1"><span class="text codedescription">{{ upload.codedescription || upload.codeId }}</span></div>
<div class="pure-u-1-3"><span class="text">{{ upload.codeId }}</span></div>
<div class="pure-u-1-3">
<span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !upload.username }">
{{ upload.username || $t('admin.uploads.userDeleted') }}
</span>
</div>
<div class="pure-u-1-3 right"><span class="text">{{ upload.created | formatDateTime }}</span></div>
</div>
<div class="pure-u-1-3 right"><span class="text">{{ upload.created | formatDateTime }}</span></div>
</div>
</div>
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item" v-if="confirmDelete == upload.id"><a href="#" class="pure-menu-link" @click.prevent="cancelDelete"><fa icon="ban"></fa> {{ $t('admin.cancel') }}</a></li>
<li class="pure-menu-item"><a href="#" class="pure-menu-link" :class="{ confirmDelete: confirmDelete == upload.id }" @click.prevent="deleteClick(upload.id)"><fa icon="trash-alt"></fa> {{ $t('admin.delete') }}</a></li>
</ul>
</div>
<div class="pure-menu pure-menu-horizontal" v-if="index == 0">
<ul class="pure-menu-list">
<li class="pure-menu-item"><a href="#" class="pure-menu-link" @click.prevent="showAssign = !showAssign"><fa icon="user"></fa> {{ $t('admin.uploads.assign') }}</a></li>
<li class="pure-menu-item" v-if="confirmDelete == upload.codeId"><a href="#" class="pure-menu-link" @click.prevent="cancelDelete"><fa icon="ban"></fa> {{ $t('admin.cancel') }}</a></li>
<li class="pure-menu-item"><a href="#" class="pure-menu-link" :class="{ confirmDelete: confirmDelete == upload.codeId }" @click.prevent="deleteClick(upload.codeId)"><fa icon="trash-alt"></fa> {{ $t('admin.delete') }}</a></li>
</ul>
</div>
<div class="file" v-for="file in upload.files" :title="file.name">
<a :href="getDownloadUrl(file)">
<img :src="getFileIconUrl(file.name)" class="icon">
<span class="filename">{{ file.name }}</span>
<span class="size">{{ file.size | formatSizeSI }}</span>
</a>
</div>
<div v-if="showAssign" class="assign pure-form pure-form-horizontal">
<select v-model="assignUser">
<option v-for="user in assignUsers" v-if="user.id !== upload.userId" :value="user.id">{{ user.name }}</option>
</select>
<button class="pure-button pure-button-primary" @click.prevent="assignCode(upload.codeId)">{{ $t('admin.uploads.assignApply') }}</button>
<button class="pure-button" @click.prevent="showAssign = false">{{ $t('admin.cancel') }}</button>
</div>
<div class="file" v-for="file in upload.files" :title="file.name">
<a :href="getDownloadUrl(file)">
<img :src="getFileIconUrl(file.name)" class="icon">
<span class="filename">{{ file.name }}</span>
<span class="size">{{ file.size | formatSizeSI }}</span>
</a>
</div>
</template>
</div>
<div v-if="uploads.length == 0" class="nodata">
@ -45,6 +57,7 @@
<script>
import orderBy from 'lodash/orderBy';
import findIndex from 'lodash/findIndex';
import forEach from 'lodash/forEach';
import axios from 'axios';
import shared from '../../shared';
@ -53,7 +66,10 @@ export default {
{
return {
uploads: null,
confirmDelete: null
confirmDelete: null,
showAssign: false,
assignUser: null,
assignUsers: []
};
},
@ -78,8 +94,47 @@ export default {
self.uploads = orderBy(response.data, ['created'], ['desc']);
})
.catch((error) => { shared.$emit('apiError', error, this.$router) });
axios.get('/admin/assign/users', {
headers: {
Authorization: 'Bearer ' + shared.adminToken
}
})
.then((response) =>
{
self.assignUsers = orderBy(response.data, ['name'], ['asc']);
})
.catch((error) => { shared.$emit('apiError', error, this.$router) });
},
computed: {
groupedUploads()
{
let self = this;
let codes = {};
// Get unique codes and their highest upload time
forEach(self.uploads, (upload) =>
{
if (!codes.hasOwnProperty(upload.codeId))
{
codes[upload.codeId] = {
maxCreated: upload.created,
uploads: [upload]
};
}
else
// uploads is sorted by created descending, so no need to update maxCreated
codes[upload.codeId].uploads.push(upload);
});
return orderBy(codes, ['maxCreated'], ['desc']);
}
},
methods: {
hasAuth(token)
{
@ -110,29 +165,26 @@ export default {
},
deleteClick(uploadId)
deleteClick(codeId)
{
var self = this;
if (self.confirmDelete == uploadId)
if (self.confirmDelete == codeId)
{
self.confirmDelete = null;
axios.delete('/admin/uploads/' + encodeURIComponent(uploadId), {
axios.delete('/admin/codeuploads/' + encodeURIComponent(codeId), {
headers: {
Authorization: 'Bearer ' + shared.adminToken
}})
.then((response) =>
{
var index = findIndex(self.uploads, (item) => { return item.id == uploadId; });
if (index > -1)
self.uploads.splice(index, 1);
})
.catch((error) => { shared.$emit('apiError', error, this.$router) });
}
else
{
self.confirmDelete = uploadId;
self.confirmDelete = codeId;
}
},
@ -140,6 +192,72 @@ export default {
{
var self = this;
self.confirmDelete = null;
},
assignCode(codeId)
{
var self = this;
let userId = self.assignUser;
if (!userId)
return;
self.assignUser = null;
self.showAssign = false;
axios.post('/admin/assign/code', {
id: codeId,
userId: userId
}, {
headers: {
Authorization: 'Bearer ' + shared.adminToken
}
})
.then((response) =>
{
if (self.hasAuth('viewAllUploads'))
{
let username = null;
forEach(self.assignUsers, (user) =>
{
if (user.id == userId)
username = user.name;
})
self.updateCodeUser(codeId, userId, username);
}
else
self.removeCodeUploads(codeId);
})
.catch((error) => { shared.$emit('apiError', error, this.$router) })
},
removeCodeUploads(codeId)
{
var self = this;
for (let i = self.uploads.length - 1; i >= 0; i--)
{
if (self.uploads[i].codeId == codeId)
self.uploads.splice(i, 1);
}
},
updateCodeUser(codeId, userId, username)
{
var self = this;
forEach(self.uploads, (upload) =>
{
if (upload.codeId == codeId)
{
upload.userId = userId;
upload.username = username;
}
});
}
}
}
@ -207,4 +325,10 @@ export default {
{
font-weight: bold;
}
.assign
{
margin-top: .5rem;
}
</style>