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 : ''%>:
+
+ <% uploads.forEach((upload) => { %>
+ <% upload.files.forEach((file) => { %>
+ <%=file.name%> (<%=humanFileSize(file.size, true)%>)
+ <%
+ });
+
+ if (upload.expirationDate !== null && (expirationDate === null || upload.expirationDate < expirationDate))
+ expirationDate = upload.expirationDate;
+ %>
+ <% }) %>
+
+<% 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:
<% upload.files.forEach((file) => { %>
- <%= file.name %> (<%= humanFileSize(file.size, true) %>)
+ <%=file.name%> (<%=humanFileSize(file.size, true)%>)
<% }) %>
<% 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 @@
-
-
-
-
{{ upload.codedescription || upload.code }}
-
{{ upload.code }}
-
-
- {{ upload.username || $t('admin.uploads.userDeleted') }}
-
+
+
+
+
+
{{ upload.codedescription || upload.codeId }}
+
{{ upload.codeId }}
+
+
+ {{ upload.username || $t('admin.uploads.userDeleted') }}
+
+
+
{{ upload.created | formatDateTime }}
-
{{ upload.created | formatDateTime }}
-
-
+
-
+
+
+ {{ user.name }}
+
+
+ {{ $t('admin.uploads.assignApply') }}
+ {{ $t('admin.cancel') }}
+
+
+
+
@@ -45,6 +57,7 @@