diff --git a/config.example.js b/config.example.js index ed76327..12071db 100644 --- a/config.example.js +++ b/config.example.js @@ -15,5 +15,12 @@ module.exports = { url: '/files' }, - jwtSecret: 'change me to a random generated string' + jwtSecret: 'change me to a random generated string', + + code: { + // Use https://alex7kom.github.io/nano-nanoid-cc/ to check for the chance of collisions. + // If a collision occurs, a new code will be generated and tested again for up to 100 times. + alphabet: '1234567890abcdef', + length: 8 + } }; \ No newline at end of file diff --git a/lib/api/admin.js b/lib/api/admin.js index 7f6ebfe..400f0b9 100644 --- a/lib/api/admin.js +++ b/lib/api/admin.js @@ -6,6 +6,7 @@ const path = require('path'); const resolvePath = require('resolve-path'); const AuthTokens = require('../authtokens'); const disk = require('diskusage'); +const fs = require('mz/fs'); async function checkAuthorization(req, res, repository, onVerified) @@ -111,7 +112,7 @@ module.exports = (repository) => await checkAuthorization(req, res, repository, async (user) => { var codes = await repository.codes.getCodes(user.hasAuth(AuthTokens.ViewAllCodes) ? null : user.userId); - var usernames = await repository.users.getUserNames(); + var usernames = await repository.users.getNames(); codes.forEach((item) => { @@ -190,11 +191,13 @@ module.exports = (repository) => await checkAuthorization(req, res, repository, async (user) => { var files = await repository.uploads.getUploads(user.hasAuth(AuthTokens.ViewAllUploads) ? null : user.userId); - var usernames = await repository.users.getUserNames(); + var usernames = await repository.users.getNames(); + var codedescriptions = await repository.codes.getDescriptions(); files.forEach((item) => { - item.username = usernames[item.userId]; + item.username = item.userId !== null ? usernames[item.userId] : null; + item.codedescription = item.code !== null ? codedescriptions[item.code] : null; }); res.send(files); @@ -202,6 +205,41 @@ module.exports = (repository) => })); + router.delete('/uploads/:id', asyncHandler(async (req, res) => + { + await checkAuthorization(req, res, repository, async (user) => + { + var upload = await repository.uploads.getUpload(req.params.id); + if (upload == null || (upload.userId !== user.userId && !user.hasAuth(AuthTokens.ViewAllUploads))) + { + res.sendStatus(404); + return; + } + + repository.uploads.delete(upload.id); + + await Promise.all(upload.files.map(async (file) => + { + if (!file.id) return; + if (!(await repository.uploads.fileExists(file.id))) + { + var fullpath = resolvePath(config.fileUpload.path, file.id); + try + { + await fs.unlink(fullpath); + } + catch (err) + { + console.log('Failed to delete ' + fullpath); + } + } + })); + + res.sendStatus(200); + }); + })); + + router.get('/download/:fileid/:displayname', asyncHandler(async (req, res) => { await checkAuthorization(req, res, repository, async (user) => diff --git a/lib/repository/code.js b/lib/repository/code.js index fde2cc6..3ca760c 100644 --- a/lib/repository/code.js +++ b/lib/repository/code.js @@ -1,10 +1,8 @@ const _ = require('lodash'); const retry = require('async-retry'); -const shortid = require('shortid'); +const generate = require('nanoid/generate'); const markdown = require('markdown').markdown; - - -shortid.characters('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.'); +const config = require('../../config'); class Code @@ -70,7 +68,7 @@ class CodeRepository return await retry(async bail => { - var codeId = shortid.generate(); + var codeId = generate(config.code.alphabet, config.code.length); if ((await self.findCodeUserId(codeId)) !== null) throw new Error('Code ' + codeId + ' already exists'); @@ -145,6 +143,33 @@ class CodeRepository } + getDescriptions() + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.find({}, (err, docs) => + { + if (err) + { + reject(err); + return; + } + + var descriptions = {}; + docs.forEach((dbCode) => + { + if (dbCode.description) + descriptions[dbCode._id] = dbCode.description; + }); + + resolve(descriptions); + }); + }); + } + + getCode(codeId) { var self = this; diff --git a/lib/repository/upload.js b/lib/repository/upload.js index 3098425..24dc9fe 100644 --- a/lib/repository/upload.js +++ b/lib/repository/upload.js @@ -9,6 +9,7 @@ class Upload self.id = values.id || values._id || null; self.userId = values.userId || null; + self.code = values.code || null; self.created = values.created || new Date(); self.expiration = values.expiration || null; self.files = values.files || []; @@ -107,6 +108,66 @@ class UploadRepository }); }); } + + + getUpload(uploadId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.findOne({ _id: uploadId }, (err, doc) => + { + if (err) + { + reject(err); + return; + } + + resolve(doc !== null ? new Upload(doc) : null); + }); + }); + } + + + delete(uploadId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.remove({ _id: uploadId }, (err, numRemoved) => + { + if (err) + { + reject(err); + return; + } + + resolve(); + }); + }); + } + + + fileExists(fileId) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.findOne({ 'files.id': fileId }, (err, doc) => + { + if (err) + { + reject(err); + return; + } + + resolve(doc !== null); + }); + }); + } } diff --git a/lib/repository/user.js b/lib/repository/user.js index 01198a8..bf49de6 100644 --- a/lib/repository/user.js +++ b/lib/repository/user.js @@ -165,7 +165,7 @@ class UserRepository } - getUserNames() + getNames() { var self = this; diff --git a/package-lock.json b/package-lock.json index 5830ed4..1bb4f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -184,6 +184,11 @@ "integrity": "sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI=", "dev": true }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -7455,6 +7460,16 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "1.3.0", + "object-assign": "4.1.1", + "thenify-all": "1.6.0" + } + }, "namespace-emitter": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", @@ -7472,6 +7487,11 @@ "integrity": "sha512-dndRmy03JQEN+Nh6WjQl7/OstIozeEmrtWe4TE7mEqJ8W8oMD8m2tHjsLPWt//e3hLAeRSbs4pxMyc5pk/nCkQ==", "dev": true }, + "nanoid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.0.2.tgz", + "integrity": "sha512-sCTwJt690lduNHyqknXJp8pRwzm80neOLGaiTHU2KUJZFVSErl778NNCIivEQCX5gNT0xR1Jy3HEMe/TABT6lw==" + }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", @@ -13912,11 +13932,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "shortid": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.8.tgz", - "integrity": "sha1-AzsRfWoul1gE9vCWnb59PQs1UTE=" - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -14605,6 +14620,22 @@ "integrity": "sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==", "dev": true }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "1.3.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": "3.3.0" + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index c4db06d..e7f6055 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,11 @@ "markdown": "^0.5.0", "mkdirp": "^0.5.1", "moment": "^2.22.1", + "mz": "^2.7.0", + "nanoid": "^1.0.2", "nedb": "^1.8.0", "npm": "^5.8.0", "resolve-path": "^1.4.0", - "shortid": "^2.2.8", "tus-node-server": "^0.2.10" }, "devDependencies": { diff --git a/public/src/App.vue b/public/src/App.vue index 171f1a5..106d829 100644 --- a/public/src/App.vue +++ b/public/src/App.vue @@ -93,6 +93,16 @@ body font-size: 12pt; } +a, a:hover +{ + text-decoration: none; +} + +a +{ + color: #228dd4; +} + /* pure-g uses hardcoded sans-serif to get accurate alignment, this class is added as a workaround for text in rows diff --git a/public/src/app.js b/public/src/app.js index 44ece72..15a7a8d 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -96,13 +96,13 @@ const router = new VueRouter({ }, { path: '/admin', component: () => import('./route/admin/Landing.vue'), children: [ - { path: 'uploads', component: () => import('./route/admin/Uploads.vue') }, + { path: 'uploads', name: 'adminDefault', 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') }, - { path: '', component: () => import('./route/admin/Login.vue') } + { path: '', name: 'adminRoot', component: () => import('./route/admin/Login.vue') } ] }, { path: '*', redirect: '/' } @@ -112,14 +112,21 @@ const router = new VueRouter({ router.beforeEach((to, from, next) => { - if (to.path.startsWith('/admin/') && to.path != '/admin/') + let isAdminRoot = to.name == 'adminRoot'; + + if (to.path.startsWith('/admin/') && !isAdminRoot) { if (!shared.adminToken) { - router.push('/admin'); + router.push({ name: 'adminRoot' }); return; } } + else if (isAdminRoot && shared.adminToken) + { + router.push({ name: 'adminDefault' }); + return; + } next(); }); diff --git a/public/src/locale/en.js b/public/src/locale/en.js index 0e45835..44613c8 100644 --- a/public/src/locale/en.js +++ b/public/src/locale/en.js @@ -31,6 +31,7 @@ export default { logout: 'Logout', cancel: 'Cancel', save: 'Save', + delete: 'Delete', diskspace: '%{available} disk space available of %{total} total', @@ -63,6 +64,7 @@ export default { detail: { code: 'Code', + codeHint: 'Direct link to the upload page for this code:', owner: 'Owner', created: 'Date created', description: 'Description', diff --git a/public/src/locale/nl.js b/public/src/locale/nl.js index 321886c..1a499cc 100644 --- a/public/src/locale/nl.js +++ b/public/src/locale/nl.js @@ -31,6 +31,7 @@ export default { logout: 'Uitloggen', cancel: 'Annuleren', save: 'Opslaan', + delete: 'Verwijderen', diskspace: '%{available} schijfruimte beschikbaar van %{total} totaal', @@ -63,6 +64,7 @@ export default { detail: { code: 'Code', + codeHint: 'Directe link naar de upload pagina voor deze code:', owner: 'Eigenaar', created: 'Datum aangemaakt', description: 'Omschrijving', diff --git a/public/src/route/admin/CodeDetail.vue b/public/src/route/admin/CodeDetail.vue index 4a29b38..a8bca5c 100644 --- a/public/src/route/admin/CodeDetail.vue +++ b/public/src/route/admin/CodeDetail.vue @@ -7,6 +7,10 @@ +
+
{{ $t('admin.codes.detail.codeHint') }}
+ {{ getCodeUrl(code.id) }} +
@@ -23,7 +27,7 @@
- {{ $t('admin.codes.detail.descriptionHint' )}} + {{ $t('admin.codes.detail.descriptionHint') }}