Admin panel
Added adding and editing of codes Added code description and message Added file sizes Added disk usage footer
This commit is contained in:
parent
5d454ea7c7
commit
45ae38bd66
|
@ -5,6 +5,7 @@ const jwt = require('jsonwebtoken');
|
|||
const path = require('path');
|
||||
const resolvePath = require('resolve-path');
|
||||
const AuthTokens = require('../authtokens');
|
||||
const disk = require('diskusage');
|
||||
|
||||
|
||||
async function checkAuthorization(req, res, repository, onVerified)
|
||||
|
@ -84,6 +85,27 @@ module.exports = (repository) =>
|
|||
}));
|
||||
|
||||
|
||||
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) =>
|
||||
|
@ -101,6 +123,68 @@ module.exports = (repository) =>
|
|||
}));
|
||||
|
||||
|
||||
router.get('/codes/:code', asyncHandler(async (req, res) =>
|
||||
{
|
||||
await checkAuthorization(req, res, repository, async (user) =>
|
||||
{
|
||||
var code = await repository.codes.getCode(req.params.code);
|
||||
if (code === null || (code.userId !== user.userId && !user.hasAuth(AuthTokens.ViewAllCodes)))
|
||||
{
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await repository.users.getUser(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 (postedCode.id)
|
||||
{
|
||||
var code = await repository.codes.getCode(postedCode.id);
|
||||
if (code === null || (code.userId !== user.userId && !user.hasAuth(AuthTokens.ViewAllCodes)))
|
||||
{
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.codes.updateCode({
|
||||
id: postedCode.id,
|
||||
expiration: postedCode.expiration,
|
||||
description: postedCode.description,
|
||||
message: postedCode.message
|
||||
});
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
else
|
||||
{
|
||||
var codeId = await repository.codes.addCode({
|
||||
userId: user.id,
|
||||
expiration: postedCode.expiration,
|
||||
description: postedCode.description,
|
||||
message: postedCode.message
|
||||
});
|
||||
}
|
||||
|
||||
res.send(codeId);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
/*
|
||||
Uploads
|
||||
*/
|
||||
router.get('/uploads', asyncHandler(async (req, res) =>
|
||||
{
|
||||
await checkAuthorization(req, res, repository, async (user) =>
|
||||
|
|
|
@ -2,6 +2,9 @@ const config = require('../../config');
|
|||
const express = require('express');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const resolvePath = require('resolve-path');
|
||||
const fs = require('fs');
|
||||
const async = require('async');
|
||||
|
||||
|
||||
async function checkAuthorization(req, res, onVerified)
|
||||
|
@ -23,7 +26,7 @@ async function checkAuthorization(req, res, onVerified)
|
|||
return;
|
||||
}
|
||||
|
||||
if (decoded.codeUserId)
|
||||
if (decoded.code)
|
||||
await onVerified(decoded);
|
||||
else
|
||||
res.sendStatus(400);
|
||||
|
@ -43,6 +46,30 @@ module.exports = (repository, tusServer) =>
|
|||
var router = express.Router();
|
||||
|
||||
// Upload API
|
||||
router.get('/message/:code', asyncHandler(async (req, res) =>
|
||||
{
|
||||
var code = await repository.codes.getCode(req.params.code);
|
||||
if (code === null)
|
||||
{
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!code.messageHTML)
|
||||
{
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await repository.users.getUser(code.userId);
|
||||
var name = user !== null ? user.name : null;
|
||||
|
||||
res.send({
|
||||
name: name,
|
||||
message: code.messageHTML
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/complete', asyncHandler(async (req, res) =>
|
||||
{
|
||||
if (!req.body.files)
|
||||
|
@ -54,8 +81,33 @@ module.exports = (repository, tusServer) =>
|
|||
await checkAuthorization(req, res, async (decoded) =>
|
||||
{
|
||||
var expiration = null; // TODO set expiration properties
|
||||
var uploadId = await repository.uploads.addUpload(decoded.codeUserId, req.body.files, expiration);
|
||||
res.send({ id: uploadId });
|
||||
|
||||
async.each(req.body.files, (item, callback) =>
|
||||
{
|
||||
if (!item.id)
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var fullpath = resolvePath(config.fileUpload.path, item.id);
|
||||
fs.stat(fullpath, (err, stats) =>
|
||||
{
|
||||
item.size = stats.size;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
async (err) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
|
||||
var uploadId = await repository.uploads.addUpload(decoded.codeUserId, decoded.code, req.body.files, expiration);
|
||||
res.send({ id: uploadId });
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
const _ = require('lodash');
|
||||
const retry = require('async-retry');
|
||||
const shortid = require('shortid');
|
||||
const markdown = require('markdown').markdown;
|
||||
|
||||
|
||||
shortid.characters('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.');
|
||||
|
||||
|
||||
class Code
|
||||
|
@ -11,6 +17,9 @@ class Code
|
|||
self.userId = values.userId || null;
|
||||
self.created = values.created || new Date();
|
||||
self.expiration = values.expiration || null;
|
||||
self.description = values.description || null;
|
||||
self.message = values.message || null;
|
||||
self.messageHTML = values.messageHTML || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,20 +39,7 @@ class CodeRepository
|
|||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
// Initialize database if empty
|
||||
self.store.count({}, (err, count) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
self.addUser('admin', null, 'test', null);
|
||||
|
||||
resolve();
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -68,25 +64,28 @@ class CodeRepository
|
|||
}
|
||||
|
||||
|
||||
async addCode(userId, expiration)
|
||||
async addCode(code)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
return await retry(async bail =>
|
||||
{
|
||||
var code = shortid.generate();
|
||||
var codeId = shortid.generate();
|
||||
|
||||
if ((await self.findCodeUserId(code)) !== null)
|
||||
throw new Error('Code ' + code + ' already exists');
|
||||
if ((await self.findCodeUserId(codeId)) !== null)
|
||||
throw new Error('Code ' + codeId + ' already exists');
|
||||
|
||||
self.store.insert({
|
||||
_id: code,
|
||||
userId: userId,
|
||||
created: new Date(),
|
||||
expiration: expiration
|
||||
_id: codeId,
|
||||
userId: code.userId,
|
||||
created: code.created || new Date(),
|
||||
expiration: code.expiration,
|
||||
description: code.description,
|
||||
message: code.message,
|
||||
messageHTML: self.getMessageHTML(code.message)
|
||||
})
|
||||
|
||||
return code;
|
||||
return codeId;
|
||||
}, {
|
||||
retries: 100,
|
||||
minTimeout: 0,
|
||||
|
@ -95,6 +94,37 @@ class CodeRepository
|
|||
}
|
||||
|
||||
|
||||
updateCode(code)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
self.store.update({ _id: code.id }, { $set: {
|
||||
expiration: code.expiration,
|
||||
description: code.description,
|
||||
message: code.message,
|
||||
messageHTML: self.getMessageHTML(code.message)
|
||||
}},
|
||||
(err, numAffected) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numAffected == 0)
|
||||
{
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getCodes(userId)
|
||||
{
|
||||
var self = this;
|
||||
|
@ -113,6 +143,32 @@ class CodeRepository
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getCode(codeId)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
self.store.findOne({ _id: codeId }, (err, doc) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(doc !== null ? new Code(doc) : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getMessageHTML(message)
|
||||
{
|
||||
return message !== null ? markdown.toHTML(message) : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ const _ = require('lodash');
|
|||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const Datastore = require('nedb');
|
||||
const shortid = require('shortid');
|
||||
const retry = require('async-retry');
|
||||
|
||||
const UserRepository = require('./user').UserRepository;
|
||||
const CodeRepository = require('./code').CodeRepository;
|
||||
|
|
|
@ -49,7 +49,7 @@ class UploadRepository
|
|||
}
|
||||
|
||||
|
||||
addUpload(userId, files, expiration)
|
||||
addUpload(userId, code, files, expiration)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
|
@ -58,10 +58,11 @@ class UploadRepository
|
|||
var upload = {
|
||||
created: new Date(),
|
||||
userId: userId,
|
||||
code: code,
|
||||
expiration: expiration,
|
||||
files: _.map(_.filter(files,
|
||||
(file) => file.hasOwnProperty('id') && file.hasOwnProperty('name')),
|
||||
(file) => { return { id: file.id, name: file.name } })
|
||||
(file) => { return { id: file.id, name: file.name, size: file.size } })
|
||||
};
|
||||
|
||||
if (upload.files.length)
|
||||
|
|
|
@ -91,7 +91,7 @@ class UserRepository
|
|||
|
||||
if (doc == null)
|
||||
{
|
||||
resolve(false);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -121,13 +121,7 @@ class UserRepository
|
|||
return;
|
||||
}
|
||||
|
||||
if (doc == null)
|
||||
{
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(new User(doc));
|
||||
resolve(doc !== null ? new User(doc) : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,6 +4,32 @@
|
|||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome/-/fontawesome-1.1.5.tgz",
|
||||
"integrity": "sha512-WAgbcVs7/YTxq7RK/dhyoJPzaIZpOQnStyO5s1sj0rZa0J1ScXYoGPmsP1ec6qM/BhDjRVB228xr2DiCPTHRCA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "0.1.4"
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.4.tgz",
|
||||
"integrity": "sha512-JEgIoqh9HAQWul8CVLrOmWUI8nUQfCJZygRJ1Cr2H/O3+pCpVszW199cIpc7k7Nr1HJtLD77AUZVnxaKllx7AQ=="
|
||||
},
|
||||
"@fortawesome/fontawesome-free-solid": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.10.tgz",
|
||||
"integrity": "sha512-RFxCG49xJcU7NwcEjAnJaerlzCzCX5sx9HdbHt3E1zML2skwmAZ2PwqyEfuT0iW2ZfYyx6SU1Zf0GUa7FL6f/A==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "0.1.4"
|
||||
}
|
||||
},
|
||||
"@fortawesome/vue-fontawesome": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.0.22.tgz",
|
||||
"integrity": "sha512-KKQ1hVTWkXRaSGVMNI/dZsznVJIZ8dqLA2J6X8nZAIg7F2Gs1zIzBC9qxOKm+gktFAJU2aCCR7BTCoZIdjvGNw=="
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
|
||||
|
@ -309,10 +335,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||
"dev": true
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
||||
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
|
||||
"requires": {
|
||||
"lodash": "4.17.5"
|
||||
}
|
||||
},
|
||||
"async-each": {
|
||||
"version": "1.0.1",
|
||||
|
@ -2484,6 +2512,15 @@
|
|||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
||||
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
|
||||
},
|
||||
"cookie-parser": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz",
|
||||
"integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=",
|
||||
"requires": {
|
||||
"cookie": "0.3.1",
|
||||
"cookie-signature": "1.0.6"
|
||||
}
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
|
@ -2912,6 +2949,14 @@
|
|||
"randombytes": "2.0.6"
|
||||
}
|
||||
},
|
||||
"diskusage": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/diskusage/-/diskusage-0.2.4.tgz",
|
||||
"integrity": "sha512-XCLBopqnV6FUG/DdphILleiubqVERvF1ZRqkvOIiPQeMlU6Im1nvlsYqLUosgmRz1UQOXcwuO0vgqASl7DNg+w==",
|
||||
"requires": {
|
||||
"nan": "2.10.0"
|
||||
}
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz",
|
||||
|
@ -3642,6 +3687,16 @@
|
|||
"escape-string-regexp": "1.0.5"
|
||||
}
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
|
||||
"integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "1.1.0",
|
||||
"schema-utils": "0.4.5"
|
||||
}
|
||||
},
|
||||
"filename-regex": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
||||
|
@ -6210,6 +6265,11 @@
|
|||
"integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==",
|
||||
"dev": true
|
||||
},
|
||||
"js-cookie": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
|
||||
"integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
|
@ -6998,6 +7058,24 @@
|
|||
"object-visit": "1.0.1"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz",
|
||||
"integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=",
|
||||
"requires": {
|
||||
"nopt": "2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"nopt": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
|
||||
"integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=",
|
||||
"requires": {
|
||||
"abbrev": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"math-clamp-x": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/math-clamp-x/-/math-clamp-x-1.2.0.tgz",
|
||||
|
@ -7335,6 +7413,11 @@
|
|||
"minimist": "0.0.8"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.22.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
|
||||
"integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -7381,8 +7464,7 @@
|
|||
"nan": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
|
||||
},
|
||||
"nan-x": {
|
||||
"version": "1.0.2",
|
||||
|
@ -13491,6 +13573,15 @@
|
|||
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
|
||||
"dev": true
|
||||
},
|
||||
"resolve-path": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz",
|
||||
"integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=",
|
||||
"requires": {
|
||||
"http-errors": "1.6.2",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
},
|
||||
"resolve-url": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||
|
@ -17497,6 +17588,14 @@
|
|||
"recast": "0.12.9",
|
||||
"temp": "0.8.3",
|
||||
"write-file-atomic": "1.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
|
|
|
@ -15,17 +15,24 @@
|
|||
"author": "Mark van Renswoude <mark@x2software.net>",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.5",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.10",
|
||||
"@fortawesome/vue-fontawesome": "0.0.22",
|
||||
"async": "^2.6.0",
|
||||
"async-retry": "^1.2.1",
|
||||
"bcrypt": "^1.0.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"debug": "^3.1.0",
|
||||
"diskusage": "^0.2.4",
|
||||
"express": "^4.16.3",
|
||||
"express-async-handler": "^1.1.2",
|
||||
"js-cookie": "^2.2.0",
|
||||
"jsonwebtoken": "^8.2.0",
|
||||
"lodash": "^4.17.5",
|
||||
"markdown": "^0.5.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.22.1",
|
||||
"nedb": "^1.8.0",
|
||||
"npm": "^5.8.0",
|
||||
"resolve-path": "^1.4.0",
|
||||
|
@ -37,6 +44,7 @@
|
|||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"file-loader": "^1.1.11",
|
||||
"html-webpack-plugin": "^3.1.0",
|
||||
"node-sass": "^4.8.3",
|
||||
"purecss": "^1.0.0",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import _ from 'lodash';
|
||||
import shared from './shared';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
data() {
|
||||
|
@ -65,10 +66,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../node_modules/purecss/build/base.css';
|
||||
@import '../../node_modules/purecss/build/buttons.css';
|
||||
@import '../../node_modules/purecss/build/forms.css';
|
||||
@import '../../node_modules/purecss/build/grids.css';
|
||||
@import '~purecss/build/base.css';
|
||||
@import '~purecss/build/buttons.css';
|
||||
@import '~purecss/build/forms.css';
|
||||
@import '~purecss/build/grids.css';
|
||||
|
||||
/* open-sans-regular - latin */
|
||||
@font-face {
|
||||
|
@ -92,6 +93,15 @@ body
|
|||
font-size: 12pt;
|
||||
}
|
||||
|
||||
/*
|
||||
pure-g uses hardcoded sans-serif to get accurate alignment, this class
|
||||
is added as a workaround for text in rows
|
||||
*/
|
||||
.text
|
||||
{
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
.container
|
||||
{
|
||||
margin-left: auto;
|
||||
|
@ -154,4 +164,13 @@ body
|
|||
font-size: 8pt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pure-form-description
|
||||
{
|
||||
font-size: 80%;
|
||||
color: #808080;
|
||||
|
||||
margin-left: 180px;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,11 @@ import VueI18n from 'vue-i18n';
|
|||
import VueRouter from 'vue-router';
|
||||
import App from './App.vue';
|
||||
import messages from './lang';
|
||||
|
||||
import shared from './shared';
|
||||
import fontawesome from '@fortawesome/fontawesome';
|
||||
import FontAwesomeIcon from '@fortawesome/vue-fontawesome';
|
||||
import solid from '@fortawesome/fontawesome-free-solid';
|
||||
import moment from 'moment'
|
||||
|
||||
if (typeof customMessages !== 'undefined')
|
||||
{
|
||||
|
@ -20,6 +24,9 @@ if (typeof customMessages !== 'undefined')
|
|||
}
|
||||
|
||||
|
||||
fontawesome.library.add(solid);
|
||||
Vue.component('fa', FontAwesomeIcon);
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(VueRouter);
|
||||
|
||||
|
@ -30,7 +37,52 @@ const i18n = new VueI18n({
|
|||
});
|
||||
|
||||
|
||||
Vue.filter('formatDateTime', (value) =>
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
return moment(String(value)).format(i18n.t('dateTimeFormat'))
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// All credit goes to: https://stackoverflow.com/a/14919494
|
||||
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];
|
||||
}
|
||||
|
||||
Vue.filter('formatSize', (value) =>
|
||||
{
|
||||
return humanFileSize(value, false);
|
||||
});
|
||||
|
||||
Vue.filter('formatSizeSI', (value) =>
|
||||
{
|
||||
return humanFileSize(value, true);
|
||||
});
|
||||
|
||||
|
||||
const Code = () => import('./route/Code.vue');
|
||||
const AdminCodeDetail = () => import('./route/admin/CodeDetail.vue');
|
||||
|
||||
|
||||
const router = new VueRouter({
|
||||
|
@ -45,6 +97,8 @@ const router = new VueRouter({
|
|||
{ path: '/admin', component: () => import('./route/admin/Landing.vue'),
|
||||
children: [
|
||||
{ path: 'uploads', component: () => import('./route/admin/Uploads.vue') },
|
||||
{ path: 'codes/add', component: AdminCodeDetail },
|
||||
{ path: 'codes/edit/:codeParam', component: AdminCodeDetail, props: true },
|
||||
{ path: 'codes', component: () => import('./route/admin/Codes.vue') },
|
||||
{ path: 'profile', component: () => import('./route/admin/Profile.vue') },
|
||||
{ path: 'users', component: () => import('./route/admin/Users.vue') },
|
||||
|
@ -55,6 +109,22 @@ const router = new VueRouter({
|
|||
]
|
||||
});
|
||||
|
||||
|
||||
router.beforeEach((to, from, next) =>
|
||||
{
|
||||
if (to.path.startsWith('/admin/') && to.path != '/admin/')
|
||||
{
|
||||
if (!shared.adminToken)
|
||||
{
|
||||
router.push('/admin');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
i18n,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
title: 'File upload - Recv',
|
||||
disclaimer: '',
|
||||
dateTimeFormat: 'MM/DD/YYYY hh:mm a',
|
||||
|
||||
landing: {
|
||||
invitePlaceholder: 'Code',
|
||||
|
@ -11,9 +12,12 @@ export default {
|
|||
},
|
||||
|
||||
notification: {
|
||||
invalidCode: 'The specified code is invalid or has expired'
|
||||
invalidCode: 'The specified code is invalid or has expired',
|
||||
invalidLogin: 'The specified username or password is incorrect'
|
||||
},
|
||||
|
||||
messageFrom: 'Message from ',
|
||||
|
||||
uppyDashboard: {
|
||||
done: 'Done',
|
||||
dropPaste: 'Drop files here, paste or',
|
||||
|
@ -25,6 +29,10 @@ export default {
|
|||
loading: 'Loading…',
|
||||
empty: 'No data',
|
||||
logout: 'Logout',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
|
||||
diskspace: '%{available} disk space available of %{total} total',
|
||||
|
||||
login: {
|
||||
usernamePlaceholder: 'Username or e-mail address',
|
||||
|
@ -41,14 +49,28 @@ export default {
|
|||
|
||||
uploads: {
|
||||
created: 'Date',
|
||||
code: 'Code',
|
||||
owner: 'Owner'
|
||||
},
|
||||
|
||||
codes: {
|
||||
code: 'Code',
|
||||
owner: 'Owner',
|
||||
add: 'Generate code',
|
||||
|
||||
add: 'Generate code'
|
||||
list: {
|
||||
code: 'Code',
|
||||
owner: 'Owner'
|
||||
},
|
||||
|
||||
detail: {
|
||||
code: 'Code',
|
||||
owner: 'Owner',
|
||||
created: 'Date created',
|
||||
description: 'Description',
|
||||
descriptionHint: 'The description will be visible only in the admin panel',
|
||||
expiration: 'Expiration',
|
||||
message: 'Message',
|
||||
messageHint: 'The message will be displayed to the user on the upload page after the code is entered. Markdown is supported.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
title: 'Bestandsoverdracht - Recv',
|
||||
disclaimer: '',
|
||||
dateTimeFormat: 'DD-MM-YYYY HH:mm',
|
||||
|
||||
landing: {
|
||||
invitePlaceholder: 'Code',
|
||||
|
@ -11,9 +12,12 @@ export default {
|
|||
},
|
||||
|
||||
notification: {
|
||||
invalidCode: 'De ingevoerde code is ongeldig of verlopen'
|
||||
invalidCode: 'De ingevoerde code is ongeldig of verlopen',
|
||||
invalidLogin: 'De ingevoerde gebruikersnaam of wachtwoord is incorrect'
|
||||
},
|
||||
|
||||
messageFrom: 'Bericht van ',
|
||||
|
||||
uppyDashboard: {
|
||||
done: 'Gereed',
|
||||
dropPaste: 'Sleep bestanden, plak of ',
|
||||
|
@ -25,6 +29,10 @@ export default {
|
|||
loading: 'Bezig met laden…',
|
||||
empty: 'Geen gegevens',
|
||||
logout: 'Uitloggen',
|
||||
cancel: 'Annuleren',
|
||||
save: 'Opslaan',
|
||||
|
||||
diskspace: '%{available} schijfruimte beschikbaar van %{total} totaal',
|
||||
|
||||
login: {
|
||||
usernamePlaceholder: 'Gebruikersnaam of e-mail adres',
|
||||
|
@ -41,14 +49,28 @@ export default {
|
|||
|
||||
uploads: {
|
||||
created: 'Datum',
|
||||
code: 'Code',
|
||||
owner: 'Eigenaar'
|
||||
},
|
||||
|
||||
codes: {
|
||||
code: 'Code',
|
||||
owner: 'Eigenaar',
|
||||
add: 'Genereer code',
|
||||
|
||||
add: 'Genereer code'
|
||||
list: {
|
||||
code: 'Code',
|
||||
owner: 'Eigenaar'
|
||||
},
|
||||
|
||||
detail: {
|
||||
code: 'Code',
|
||||
owner: 'Eigenaar',
|
||||
created: 'Datum aangemaakt',
|
||||
description: 'Omschrijving',
|
||||
descriptionHint: 'De omschrijving is alleen zichtbaar in beheer',
|
||||
expiration: 'Verloopt',
|
||||
message: 'Bericht',
|
||||
messageHint: 'Het bericht wordt getoond aan de gebruiker op de upload pagina na het invoeren van de code. Markdown kan worden gebruikt.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ export default {
|
|||
})
|
||||
.then((response) =>
|
||||
{
|
||||
shared.token = response.data;
|
||||
shared.uploadToken = response.data;
|
||||
self.$router.push({ path: '/u/' + self.code.value });
|
||||
})
|
||||
.catch((error) =>
|
||||
|
@ -72,7 +72,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../../node_modules/uppy/src/scss/uppy.scss';
|
||||
@import '~uppy/src/scss/uppy.scss';
|
||||
|
||||
|
||||
#landing
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
<template>
|
||||
<div id="upload">
|
||||
<router-link to="/">Terug</router-link>
|
||||
<div class="message" v-if="message !== null">
|
||||
<div class="from" v-if="message.name !== null">{{ $t('messageFrom') + message.name }}</div>
|
||||
<div v-html="message.message"></div>
|
||||
</div>
|
||||
|
||||
<div class="uploadTarget"></div>
|
||||
|
||||
<div class="navigation">
|
||||
<router-link to="/" class="pure-button">Terug</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,7 +29,8 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
token: shared.token
|
||||
uploadToken: shared.uploadToken,
|
||||
message: null
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -29,7 +38,7 @@ export default {
|
|||
{
|
||||
var self = this;
|
||||
|
||||
if (!self.token)
|
||||
if (!self.uploadToken)
|
||||
{
|
||||
if (self.codeParam)
|
||||
self.$router.push('/c/' + self.codeParam);
|
||||
|
@ -38,6 +47,13 @@ export default {
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
axios.get('/message/' + encodeURIComponent(self.codeParam))
|
||||
.then((response) =>
|
||||
{
|
||||
self.message = response.status !== 204 ? response.data : null;
|
||||
});
|
||||
},
|
||||
|
||||
mounted()
|
||||
|
@ -64,7 +80,7 @@ export default {
|
|||
.use(Tus, {
|
||||
endpoint: '/upload/',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + self.token
|
||||
Authorization: 'Bearer ' + self.uploadToken
|
||||
}
|
||||
})
|
||||
.run();
|
||||
|
@ -82,7 +98,7 @@ export default {
|
|||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + self.token
|
||||
Authorization: 'Bearer ' + self.uploadToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
|
@ -92,6 +108,7 @@ export default {
|
|||
.catch((error) =>
|
||||
{
|
||||
// TODO can we convince Uppy that the files actually failed?
|
||||
shared.$emit('showNotification', error.message);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -104,4 +121,21 @@ export default {
|
|||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.message
|
||||
{
|
||||
.from
|
||||
{
|
||||
color: #808080;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation
|
||||
{
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div id="code">
|
||||
<div v-if="code !== null">
|
||||
<form class="pure-form pure-form-aligned">
|
||||
<fieldset>
|
||||
<div class="pure-control-group" v-if="code.id">
|
||||
<label for="codeId">{{ $t('admin.codes.detail.code') }}</label>
|
||||
<input id="codeId" type="text" readonly :value="code.id" class="pure-input-2-3">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group" v-if="code.id">
|
||||
<label for="user">{{ $t('admin.codes.detail.owner') }}</label>
|
||||
<input id="user" type="text" readonly :value="code.username" class="pure-input-2-3">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group" v-if="code.id">
|
||||
<label for="created">{{ $t('admin.codes.detail.created') }}</label>
|
||||
<input id="created" type="text" readonly :value="code.created | formatDateTime" class="pure-input-2-3">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="description">{{ $t('admin.codes.detail.description') }}</label>
|
||||
<input id="description" type="text" v-model="code.description" class="pure-input-2-3">
|
||||
</div>
|
||||
<div class="pure-form-description">
|
||||
{{ $t('admin.codes.detail.descriptionHint' )}}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="pure-control-group">
|
||||
<label for="expiration">{{ $t('admin.codes.detail.expiration') }}</label>
|
||||
<input id="expiration" type="text" :value="code.expiration" class="pure-input-2-3">
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="message">{{ $t('admin.codes.detail.message') }}</label>
|
||||
<textarea id="message" type="text" v-model="code.message" class="pure-input-2-3" rows="10"></textarea>
|
||||
</div>
|
||||
<div class="pure-form-description">
|
||||
{{ $t('admin.codes.detail.messageHint' )}}
|
||||
</div>
|
||||
|
||||
<div class="pure-controls">
|
||||
<button class="pure-button pure-button-primary" @click="save" :disabled="saving">{{ $t('admin.save') }} <fa icon="spinner" pulse v-if="saving"></fa></button>
|
||||
<router-link to="/admin/codes" class="pure-button">{{ $t('admin.cancel') }}</router-link>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import shared from '../../shared';
|
||||
|
||||
export default {
|
||||
data()
|
||||
{
|
||||
return {
|
||||
code: null,
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
|
||||
props: ['codeParam'],
|
||||
|
||||
created()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.codeParam)
|
||||
{
|
||||
axios.get('/admin/codes/' + encodeURIComponent(self.codeParam), {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
self.code = response.data;
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) });
|
||||
}
|
||||
else
|
||||
{
|
||||
self.code = {
|
||||
expiration: null,
|
||||
message: null
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
self.saving = true;
|
||||
|
||||
axios.post('/admin/codes', {
|
||||
id: self.code.id,
|
||||
expiration: self.code.expiration,
|
||||
description: self.code.description,
|
||||
message: self.code.message
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
this.$router.push('/admin/codes');
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) })
|
||||
.then(() =>
|
||||
{
|
||||
self.saving = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
|
@ -1,38 +1,34 @@
|
|||
<template>
|
||||
<div id="codes">
|
||||
<Menu></Menu>
|
||||
<router-link to="codes/add" class="pure-button pure-button-primary">{{ $t('admin.codes.add') }}</router-link>
|
||||
|
||||
<div class="page">
|
||||
<router-link to="codes/add" class="pure-button pure-button-primary">{{ $t('admin.codes.add') }}</router-link>
|
||||
<div v-if="codes !== null">
|
||||
<div class="pure-g list-header">
|
||||
<div class="pure-u-3-4"><span class="text">{{ $t('admin.codes.list.code') }}</span></div>
|
||||
<div class="pure-u-1-4"><span class="text">{{ $t('admin.codes.list.owner') }}</span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="codes !== null">
|
||||
<div class="pure-g list-header">
|
||||
<div class="pure-u-3-4">{{ $t('admin.codes.code') }}</div>
|
||||
<div class="pure-u-1-4">{{ $t('admin.codes.owner') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="code in codes" class="list">
|
||||
<div class="pure-g row">
|
||||
<div class="pure-u-3-4">{{ code.id }}</div>
|
||||
<div class="pure-u-1-4">{{ code.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="codes.length == 0" class="nodata">
|
||||
{{ $t('admin.empty') }}
|
||||
<div v-for="code in codes" class="list">
|
||||
<div class="pure-g row">
|
||||
<div class="pure-u-3-4"><span class="text"><router-link :to="'codes/edit/' + encodeURIComponent(code.id)">{{ code.id }}</router-link></span></div>
|
||||
<div class="pure-u-1-4"><span class="text">{{ code.username }}</span></div>
|
||||
<div class="pure-u-1-1" v-if="code.description"><span class="text description">{{ code.description }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="loading">
|
||||
{{ $t('admin.loading') }}
|
||||
<div v-if="codes.length == 0" class="nodata">
|
||||
{{ $t('admin.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="loading">
|
||||
{{ $t('admin.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Menu from './Menu.vue';
|
||||
import shared from '../../shared';
|
||||
|
||||
export default {
|
||||
|
@ -43,43 +39,28 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
Menu
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (!shared.token)
|
||||
{
|
||||
self.$router.push('/admin');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
axios.get('/admin/codes', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.token
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
self.codes = response.data;
|
||||
self.codes = _.orderBy(response.data, ['created'], ['desc']);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
if (error.response && error.response.status == 403)
|
||||
{
|
||||
shared.token = null;
|
||||
self.$router.push('/admin');
|
||||
}
|
||||
else
|
||||
shared.$emit('showNotification', error.message);
|
||||
});
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.description
|
||||
{
|
||||
font-size: 75%;
|
||||
color: #808080;
|
||||
}
|
||||
</style>
|
|
@ -1,18 +1,113 @@
|
|||
<template>
|
||||
<div id="admin">
|
||||
<router-view></router-view>
|
||||
<div v-if="loggedIn">
|
||||
<Menu></Menu>
|
||||
|
||||
<div class="content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view v-else></router-view>
|
||||
|
||||
<div class="footer" v-if="loggedIn">
|
||||
<div v-if="diskspace !== null">
|
||||
{{ $t('admin.diskspace', formattedDiskspace) }}
|
||||
|
||||
<div class="diskspace">
|
||||
<div class="bar" :class="{ warning: diskspaceWarning}" :style="'width: ' + diskspacePercentage + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from './Menu.vue';
|
||||
import shared from '../../shared';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data()
|
||||
{
|
||||
return {
|
||||
diskspace: null
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
Menu
|
||||
},
|
||||
|
||||
computed: {
|
||||
loggedIn()
|
||||
{
|
||||
return shared.adminToken !== null;
|
||||
},
|
||||
|
||||
formattedDiskspace()
|
||||
{
|
||||
var self = this;
|
||||
if (self.diskspace === null)
|
||||
return null;
|
||||
|
||||
return {
|
||||
free: self.$options.filters.formatSizeSI(self.diskspace.free),
|
||||
available: self.$options.filters.formatSizeSI(self.diskspace.available),
|
||||
total: self.$options.filters.formatSizeSI(self.diskspace.total)
|
||||
}
|
||||
},
|
||||
|
||||
diskspacePercentage()
|
||||
{
|
||||
var self = this;
|
||||
return Math.floor((self.diskspace.total - self.diskspace.available) / self.diskspace.total * 100);
|
||||
},
|
||||
|
||||
diskspaceWarning()
|
||||
{
|
||||
var self = this;
|
||||
return self.diskspacePercentage >= 80;
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
var self = this;
|
||||
self.unwatch = self.$watch(self.updateDiskSpace, () => self.updateDiskSpace());
|
||||
},
|
||||
|
||||
beforeDestroy()
|
||||
{
|
||||
var self = this;
|
||||
self.unwatch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateDiskSpace()
|
||||
{
|
||||
var self = this;
|
||||
if (shared.adminToken !== null)
|
||||
{
|
||||
axios.get('/admin/diskspace', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}})
|
||||
.then((response) =>
|
||||
{
|
||||
self.diskspace = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page
|
||||
#admin .content
|
||||
{
|
||||
margin-top: 3rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nodata
|
||||
|
@ -30,7 +125,7 @@ $list-padding: .2rem;
|
|||
|
||||
.list-header
|
||||
{
|
||||
background-color: #e0e0e0;
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
|
||||
margin-top: 1rem;
|
||||
|
@ -45,4 +140,51 @@ $list-padding: .2rem;
|
|||
padding: $list-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.footer
|
||||
{
|
||||
font-size: 75%;
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.diskspace
|
||||
{
|
||||
border: solid 1px black;
|
||||
width: 350px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
height: 21px;
|
||||
|
||||
/* Thanks http://www.colorzilla.com/gradient-editor/ ! */
|
||||
background: #f6f8f9;
|
||||
background: -moz-linear-gradient(top, #f6f8f9 0%, #e5ebee 50%, #d7dee3 51%, #f5f7f9 100%);
|
||||
background: -webkit-linear-gradient(top, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%);
|
||||
background: linear-gradient(to bottom, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f6f8f9', endColorstr='#f5f7f9',GradientType=0 );
|
||||
|
||||
.bar
|
||||
{
|
||||
height: 21px;
|
||||
|
||||
background: #b7deed;
|
||||
background: -moz-linear-gradient(top, #b7deed 0%, #71ceef 50%, #21b4e2 51%, #b7deed 100%);
|
||||
background: -webkit-linear-gradient(top, #b7deed 0%,#71ceef 50%,#21b4e2 51%,#b7deed 100%);
|
||||
background: linear-gradient(to bottom, #b7deed 0%,#71ceef 50%,#21b4e2 51%,#b7deed 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b7deed', endColorstr='#b7deed',GradientType=0 );
|
||||
|
||||
&.warning
|
||||
{
|
||||
background: #feccb1;
|
||||
background: -moz-linear-gradient(top, #feccb1 0%, #f17432 50%, #ea5507 51%, #fb955e 100%);
|
||||
background: -webkit-linear-gradient(top, #feccb1 0%,#f17432 50%,#ea5507 51%,#fb955e 100%);
|
||||
background: linear-gradient(to bottom, #feccb1 0%,#f17432 50%,#ea5507 51%,#fb955e 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#feccb1', endColorstr='#fb955e',GradientType=0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -12,7 +12,7 @@
|
|||
<input type="password" class="pure-input-1-2" v-model="password" :placeholder="$t('admin.login.passwordPlaceholder')">
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" class="pure-button pure-button-primary" :disabled="username.trim() == '' || password.trim() == '' || checking">{{ $t(checking ? 'admin.login.buttonChecking' : 'admin.login.button') }} <span v-if="checking"><i class="fas fa-spinner fa-pulse"></i></span></button>
|
||||
<button type="submit" class="pure-button pure-button-primary" :disabled="username.trim() == '' || password.trim() == '' || checking">{{ $t(checking ? 'admin.login.buttonChecking' : 'admin.login.button') }} <fa icon="spinner" pulse v-if="checking"></fa></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,13 +48,13 @@ export default {
|
|||
})
|
||||
.then((response) =>
|
||||
{
|
||||
shared.token = response.data;
|
||||
shared.adminToken = response.data;
|
||||
self.$router.push({ path: '/admin/uploads' });
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
if (error.response && error.response.status == 403)
|
||||
shared.$emit('showNotification', self.$i18n.t('notification.invalidCode'), false);
|
||||
shared.$emit('showNotification', self.$i18n.t('notification.invalidLogin'), false);
|
||||
else
|
||||
shared.$emit('showNotification', error.message);
|
||||
})
|
||||
|
|
|
@ -41,23 +41,21 @@ export default {
|
|||
methods: {
|
||||
hasAuth(token)
|
||||
{
|
||||
return shared.user.auth.indexOf(token) > -1;
|
||||
return shared.user !== null && shared.user.auth.indexOf(token) > -1;
|
||||
},
|
||||
|
||||
|
||||
logout()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
shared.token = null;
|
||||
self.$router.push('/admin');
|
||||
shared.adminToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../../../node_modules/purecss/build/menus.css';
|
||||
@import '~purecss/build/menus.css';
|
||||
|
||||
|
||||
#menu
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
<template>
|
||||
<div id="uploads">
|
||||
<Menu></Menu>
|
||||
<div v-if="uploads !== null">
|
||||
<div class="pure-g list-header">
|
||||
<div class="pure-u-1-3"><span class="text">{{ $t('admin.uploads.created') }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ $t('admin.uploads.code') }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ $t('admin.uploads.owner') }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<div v-if="uploads !== null">
|
||||
<div class="pure-g list-header">
|
||||
<div class="pure-u-3-4">{{ $t('admin.uploads.created') }}</div>
|
||||
<div class="pure-u-1-4">{{ $t('admin.uploads.owner') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="upload in uploads" class="list">
|
||||
<div class="pure-g row">
|
||||
<div class="pure-u-3-4">{{ upload.created }}</div>
|
||||
<div class="pure-u-1-4">{{ upload.username }}</div>
|
||||
<div class="pure-u-1-1">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="upload in uploads" class="list">
|
||||
<div class="properties">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-3"><span class="text">{{ upload.created | formatDateTime }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ upload.code }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ upload.username }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploads.length == 0" class="nodata">
|
||||
{{ $t('admin.empty') }}
|
||||
<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>
|
||||
|
||||
<div v-else class="loading">
|
||||
{{ $t('admin.loading') }}
|
||||
<div v-if="uploads.length == 0" class="nodata">
|
||||
{{ $t('admin.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="loading">
|
||||
{{ $t('admin.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
import Menu from './Menu.vue';
|
||||
import shared from '../../shared';
|
||||
|
||||
export default {
|
||||
|
@ -49,15 +49,11 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
Menu
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (!shared.token)
|
||||
if (!shared.adminToken)
|
||||
{
|
||||
self.$router.push('/admin');
|
||||
return;
|
||||
|
@ -66,23 +62,14 @@ export default {
|
|||
|
||||
axios.get('/admin/uploads', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.token
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
self.uploads = response.data;
|
||||
self.uploads = _.orderBy(response.data, ['created'], ['desc']);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
if (error.response && error.response.status == 403)
|
||||
{
|
||||
shared.token = null;
|
||||
self.$router.push('/admin');
|
||||
}
|
||||
else
|
||||
shared.$emit('showNotification', error.message);
|
||||
});
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) });
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -117,6 +104,8 @@ export default {
|
|||
display: inline-block;
|
||||
width: 10rem;
|
||||
padding: .5rem;
|
||||
margin-top: .5rem;
|
||||
border: solid 1px transparent;
|
||||
|
||||
font-size: 75%;
|
||||
|
||||
|
@ -126,6 +115,12 @@ export default {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a
|
||||
{
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon
|
||||
{
|
||||
display: block;
|
||||
|
@ -133,5 +128,30 @@ export default {
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
.filename, .size
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.size
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
|
||||
&:hover
|
||||
{
|
||||
background-color: #d3e9f8;
|
||||
border: solid 1px #a7d3f1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.properties
|
||||
{
|
||||
background-color: #f4f4f4;
|
||||
padding: .2rem;
|
||||
}
|
||||
</style>
|
|
@ -1,25 +1,15 @@
|
|||
<template>
|
||||
<div id="users">
|
||||
<Menu></Menu>
|
||||
|
||||
<div class="page">
|
||||
Users!
|
||||
</div>
|
||||
Users!
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from './Menu.vue';
|
||||
|
||||
export default {
|
||||
data()
|
||||
{
|
||||
return {
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
Menu
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -6,8 +6,9 @@ import axios from 'axios';
|
|||
export default new Vue({
|
||||
data() {
|
||||
return {
|
||||
token: null,
|
||||
user: []
|
||||
uploadToken: null,
|
||||
adminToken: null,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -15,32 +16,47 @@ export default new Vue({
|
|||
{
|
||||
var self = this;
|
||||
|
||||
var cookie = Cookies.get('token');
|
||||
var cookie = Cookies.get('adminToken');
|
||||
if (typeof cookie !== 'undefined')
|
||||
self.token = cookie;
|
||||
self.adminToken = cookie;
|
||||
|
||||
|
||||
self.$on('apiError', (error, $router) =>
|
||||
{
|
||||
if (error.response && error.response.status == 403)
|
||||
{
|
||||
self.adminToken = null;
|
||||
$router.push('/admin');
|
||||
}
|
||||
else
|
||||
self.$emit('showNotification', error.message);
|
||||
});
|
||||
},
|
||||
|
||||
watch: {
|
||||
token(newValue)
|
||||
adminToken(newValue)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (newValue !== null)
|
||||
{
|
||||
Cookies.set('token', newValue);
|
||||
Cookies.set('adminToken', newValue);
|
||||
|
||||
axios.get('/admin/whoami', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + self.token
|
||||
Authorization: 'Bearer ' + self.adminToken
|
||||
}})
|
||||
.then((response) =>
|
||||
{
|
||||
self.user = response.data;
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Cookies.remove('token');
|
||||
Cookies.remove('adminToken');
|
||||
self.user = null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue