From a07c9a3ef7c234b0d48cf11a03195b4dcacb3b47 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Wed, 21 Mar 2018 16:51:46 +0100 Subject: [PATCH] Restructured in preparation for admin panel --- index.js | 93 +++---------- lib/api/token.js | 32 +++++ lib/api/upload.js | 60 +++++++++ lib/repository.js | 208 ----------------------------- lib/repository/codes.js | 82 ++++++++++++ lib/repository/index.js | 61 +++++++++ lib/repository/uploads.js | 70 ++++++++++ lib/repository/users.js | 104 +++++++++++++++ public/src/App.vue | 12 +- public/src/app.js | 23 +++- public/src/route/Code.vue | 92 +++++++++++++ public/src/route/Landing.vue | 114 +++------------- public/src/route/admin/Codes.vue | 4 + public/src/route/admin/Landing.vue | 10 ++ public/src/route/admin/Login.vue | 10 ++ public/src/route/admin/Profile.vue | 4 + public/src/route/admin/Users.vue | 4 + 17 files changed, 593 insertions(+), 390 deletions(-) create mode 100644 lib/api/token.js create mode 100644 lib/api/upload.js delete mode 100644 lib/repository.js create mode 100644 lib/repository/codes.js create mode 100644 lib/repository/index.js create mode 100644 lib/repository/uploads.js create mode 100644 lib/repository/users.js create mode 100644 public/src/route/Code.vue create mode 100644 public/src/route/admin/Codes.vue create mode 100644 public/src/route/admin/Landing.vue create mode 100644 public/src/route/admin/Login.vue create mode 100644 public/src/route/admin/Profile.vue create mode 100644 public/src/route/admin/Users.vue diff --git a/index.js b/index.js index 273b113..5ffed89 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const config = require('./config'); const Repository = require('./lib/repository'); const _ = require('lodash'); +const fs = require('fs'); const express = require('express'); const bodyParser = require('body-parser'); const tus = require('tus-node-server'); @@ -16,33 +17,16 @@ const webpackHotMiddleware = require('webpack-hot-middleware'); const webpackConfig = require('./webpack.config.js'); -function checkAuthorization(req, res, onVerified) -{ - if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') - { - res.sendStatus(400); - return; - } - - var token = req.headers.authorization.split(' ')[1]; - jwt.verify(token, config.jwtSecret, (err, decoded) => - { - if (err) - { - res.sendStatus(403); - return; - } - - onVerified(decoded); - }); -} - - (async function() { const isDevelopment = process.env.NODE_ENV !== 'production'; const repository = new Repository(config.database); + await repository.load() + .catch((err) => + { + console.log(err); + }); const tusServer = new tus.Server(); @@ -57,66 +41,25 @@ function checkAuthorization(req, res, onVerified) app.use(bodyParser.json()); - // Token API - app.post('/token/upload', async (req, res) => + + const registerAPI = (name) => { require('./lib/api/' + name)(app, repository) } + + fs.readdir('./lib/api', (err, files) => { - if (!req.body.code) + if (err) { - res.sendStatus(400); + console.log(err); return; } - var userId = await repository.findCodeUserId(req.body.code); - if (userId !== null) + _.forEach(files, (fileName) => { - jwt.sign({ - code: req.body.code, - userId: userId - }, config.jwtSecret, (err, token) => - { - if (err) - res.sendStatus(500); - else - res.send(token); - }); - } - else - res.sendStatus(403); - }); - - - // Upload API - app.post('/complete', (req, res) => - { - if (!req.body.files) - { - res.sendStatus(400); - return; - } - - checkAuthorization(req, res, async (decoded) => - { - console.log('1'); - var uploadId = await repository.addUpload(decoded.userId, req.body.files); - console.log(uploadId); - res.send({ id: uploadId }); + console.log('Loading API ' + fileName + '...'); + registerAPI(fileName); }); }); - // Tus upload - const uploadApp = express(); - uploadApp.all('*', (req, res) => - { - checkAuthorization(req, res, (decoded) => - { - tusServer.handle(req, res); - }); - }); - - app.use('/upload', uploadApp); - - // Frontend if (isDevelopment) { @@ -131,5 +74,11 @@ function checkAuthorization(req, res, onVerified) app.use(express.static(path.join(__dirname, 'public', 'dist'))); + + // Redirects to make Vue-router URLs less quirky + app.get('/c/:code', (req, res) => { res.redirect(301, '/#/c/' + req.params.code) }); + app.get('/admin', (req, res) => { res.redirect(301, '/#/admin/') }); + + var server = app.listen(config.port, () => console.log('Recv running on port ' + server.address().port)); })(); \ No newline at end of file diff --git a/lib/api/token.js b/lib/api/token.js new file mode 100644 index 0000000..05f2a29 --- /dev/null +++ b/lib/api/token.js @@ -0,0 +1,32 @@ +const config = require('../../config'); +const jwt = require('jsonwebtoken'); + + +module.exports = (app, repository) => +{ + app.post('/token/upload', async (req, res) => + { + if (!req.body.code) + { + res.sendStatus(400); + return; + } + + var userId = await repository.codes.findCodeUserId(req.body.code); + if (userId !== null) + { + jwt.sign({ + code: req.body.code, + userId: userId + }, config.jwtSecret, (err, token) => + { + if (err) + res.sendStatus(500); + else + res.send(token); + }); + } + else + res.sendStatus(403); + }); +} \ No newline at end of file diff --git a/lib/api/upload.js b/lib/api/upload.js new file mode 100644 index 0000000..29f57c6 --- /dev/null +++ b/lib/api/upload.js @@ -0,0 +1,60 @@ +const config = require('../../config'); +const express = require('express'); +const jwt = require('jsonwebtoken'); + + +function checkAuthorization(req, res, onVerified) +{ + if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') + { + res.sendStatus(400); + return; + } + + var token = req.headers.authorization.split(' ')[1]; + jwt.verify(token, config.jwtSecret, (err, decoded) => + { + if (err) + { + res.sendStatus(403); + return; + } + + onVerified(decoded); + }); +} + + + +module.exports = (app, repository) => +{ + // Upload API + app.post('/complete', (req, res) => + { + if (!req.body.files) + { + res.sendStatus(400); + return; + } + + checkAuthorization(req, res, async (decoded) => + { + var expiration = null; // TODO set expiration properties + var uploadId = await repository.uploads.addUpload(decoded.userId, req.body.files, expiration); + res.send({ id: uploadId }); + }); + }); + + + // Tus upload + const uploadApp = express(); + uploadApp.all('*', (req, res) => + { + checkAuthorization(req, res, (decoded) => + { + tusServer.handle(req, res); + }); + }); + + app.use('/upload', uploadApp); +} \ No newline at end of file diff --git a/lib/repository.js b/lib/repository.js deleted file mode 100644 index f88bdf6..0000000 --- a/lib/repository.js +++ /dev/null @@ -1,208 +0,0 @@ -//const debug = require('debug')('recv:users'); -const _ = require('lodash'); -const path = require('path'); -const mkdirp = require('mkdirp'); -const bcrypt = require('bcrypt'); -const Datastore = require('nedb'); -const shortid = require('shortid'); -const retry = require('async-retry') - - -class Repository -{ - constructor(config) - { - var self = this; - - const initStore = (filename) => - { - var store = new Datastore({ - filename: path.join(config.path, filename), - autoload: true - }); - - store.persistence.setAutocompactionInterval(config.autocompactionInterval); - return store; - }; - - mkdirp(config.path, (err) => - { - if (err) - { - console.log(err); - return; - } - - self.db = { - users: initStore('users.db'), - codes: initStore('codes.db'), - uploads: initStore('uploads.db') - }; - - - // Initialize database if empty - self.db.users.count({}, (err, count) => - { - if (err) - { - console.log(err); - return; - } - - if (count == 0) - self.addUser('admin', null, 'test'); - }); - }); - } - - - async isValidUser(username, password) - { - var self = this; - - return new Promise((resolve, reject) => - { - self.db.users.findOne({ username: username }, (err, doc) => - { - if (err) - { - reject(err); - return; - } - - if (doc == null) - { - resolve(false); - return; - } - - bcrypt.compare(password, doc.password, (err, res) => - { - if (err) - reject(err) - else - resolve(res); - }); - }); - }); - } - - - async addUser(username, email, password) - { - var self = this; - - return new Promise((resolve, reject) => - { - bcrypt.hash(password, 10, function(err, hash) - { - if (err) - { - reject(err); - return; - } - - var user = { - username: username, - email: email, - password: hash - }; - - self.db.users.insert(user, (err, dbUser) => - { - if (err) - { - reject(err); - return; - } - - resolve(dbUser._id); - }); - }); - }); - } - - - async findCodeUserId(code) - { - var self = this; - - return new Promise((resolve, reject) => - { - self.db.codes.findOne({ _id: code }, (err, doc) => - { - if (err) - { - reject(err); - return; - } - - resolve(doc !== null ? doc.userId : null); - }); - }); - } - - - async addCode(userId, expiration) - { - var self = this; - - return await retry(async bail => - { - var code = shortid.generate(); - - if ((await self.findCodeUserId(code)) !== null) - throw new Error('Code ' + code + ' already exists'); - - self.db.codes.insert({ - _id: code, - userId: userId, - expiration: expiration - }) - - return code; - }, { - retries: 100, - minTimeout: 0, - maxTimeout: 0 - }); - } - - - async addUpload(userId, files) - { - var self = this; - - return new Promise((resolve, reject) => - { - var upload = { - files: _.map(_.filter(files, - (file) => file.hasOwnProperty('id') && file.hasOwnProperty('name')), - (file) => { return { id: file.id, name: file.name } }) - }; - console.log(upload); - - if (upload.files.length) - { - self.db.uploads.insert(upload, (err, dbUpload) => - { - console.log(dbUpload); - if (err) - { - reject(err); - return; - } - - resolve(dbUpload._id); - }); - } - else - { - reject(); - } - }); - } -} - - -module.exports = Repository; \ No newline at end of file diff --git a/lib/repository/codes.js b/lib/repository/codes.js new file mode 100644 index 0000000..03ba15a --- /dev/null +++ b/lib/repository/codes.js @@ -0,0 +1,82 @@ +class CodesRepository +{ + constructor(store) + { + var self = this; + self.store = store; + } + + + async init() + { + var self = this; + + 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(); + }); + }); + } + + + async findCodeUserId(code) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.findOne({ _id: code }, (err, doc) => + { + if (err) + { + reject(err); + return; + } + + resolve(doc !== null ? doc.userId : null); + }); + }); + } + + + async addCode(userId, expiration) + { + var self = this; + + return await retry(async bail => + { + var code = shortid.generate(); + + if ((await self.findCodeUserId(code)) !== null) + throw new Error('Code ' + code + ' already exists'); + + self.store.insert({ + _id: code, + userId: userId, + created: new Date(), + expiration: expiration + }) + + return code; + }, { + retries: 100, + minTimeout: 0, + maxTimeout: 0 + }); + } +} + + +module.exports = CodesRepository; \ No newline at end of file diff --git a/lib/repository/index.js b/lib/repository/index.js new file mode 100644 index 0000000..7872d85 --- /dev/null +++ b/lib/repository/index.js @@ -0,0 +1,61 @@ +//const debug = require('debug')('recv:users'); +const _ = require('lodash'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const bcrypt = require('bcrypt'); +const Datastore = require('nedb'); +const shortid = require('shortid'); +const retry = require('async-retry'); + +const UsersRepository = require('./users'); +const CodesRepository = require('./codes'); +const UploadsRepository = require('./uploads'); + + +class Repository +{ + constructor(config) + { + var self = this; + self.config = config; + } + + + async load() + { + var self = this; + + return new Promise((resolve, reject) => + { + const initStore = (filename) => + { + var store = new Datastore({ + filename: path.join(self.config.path, filename), + autoload: true + }); + + store.persistence.setAutocompactionInterval(self.config.autocompactionInterval); + return store; + }; + + mkdirp(self.config.path, async (err) => + { + if (err) + { + reject(err); + return; + } + + self.users = new UsersRepository(initStore('users.db')); + self.codes = new CodesRepository(initStore('codes.db')); + self.uploads = new UploadsRepository(initStore('uploads.db')); + + await self.users.init(); + resolve(); + }); + }); + } +} + + +module.exports = Repository; \ No newline at end of file diff --git a/lib/repository/uploads.js b/lib/repository/uploads.js new file mode 100644 index 0000000..13cea84 --- /dev/null +++ b/lib/repository/uploads.js @@ -0,0 +1,70 @@ +class UploadsRepository +{ + constructor(store) + { + var self = this; + self.store = store; + } + + + async init() + { + var self = this; + + 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(); + }); + }); + } + + + async addUpload(userId, files, expiration) + { + var self = this; + + return new Promise((resolve, reject) => + { + var upload = { + created: new Date(), + expiration: expiration, + files: _.map(_.filter(files, + (file) => file.hasOwnProperty('id') && file.hasOwnProperty('name')), + (file) => { return { id: file.id, name: file.name } }) + }; + + if (upload.files.length) + { + self.store.insert(upload, (err, dbUpload) => + { + if (err) + { + reject(err); + return; + } + + resolve(dbUpload._id); + }); + } + else + { + reject(); + } + }); + } +} + + +module.exports = UploadsRepository; \ No newline at end of file diff --git a/lib/repository/users.js b/lib/repository/users.js new file mode 100644 index 0000000..2800475 --- /dev/null +++ b/lib/repository/users.js @@ -0,0 +1,104 @@ +class UsersRepository +{ + constructor(store) + { + var self = this; + self.store = store; + } + + + async init() + { + var self = this; + + 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(); + }); + }); + } + + + async isValidUser(username, password) + { + var self = this; + + return new Promise((resolve, reject) => + { + self.store.findOne({ username: username }, (err, doc) => + { + if (err) + { + reject(err); + return; + } + + if (doc == null) + { + resolve(false); + return; + } + + bcrypt.compare(password, doc.password, (err, res) => + { + if (err) + reject(err) + else + resolve(res); + }); + }); + }); + } + + + async addUser(username, email, password, createdByUserId) + { + var self = this; + + return new Promise((resolve, reject) => + { + bcrypt.hash(password, 10, function(err, hash) + { + if (err) + { + reject(err); + return; + } + + var user = { + username: username, + email: email, + password: hash, + created: new Date(), + createdByUserId: createdByUserId + }; + + self.store.insert(user, (err, dbUser) => + { + if (err) + { + reject(err); + return; + } + + resolve(dbUser._id); + }); + }); + }); + } +} + + +module.exports = UsersRepository; \ No newline at end of file diff --git a/public/src/App.vue b/public/src/App.vue index f0cd7bd..03bbdac 100644 --- a/public/src/App.vue +++ b/public/src/App.vue @@ -7,17 +7,7 @@ -
- - - -
- -
- {{ $t('disclaimer') }} -
+ diff --git a/public/src/app.js b/public/src/app.js index 69c3cff..098ffe4 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -14,14 +14,27 @@ const i18n = new VueI18n({ }); -const Landing = () => import('./route/Landing.vue'); -const Upload = () => import('./route/Upload.vue'); +const Code = () => import('./route/Code.vue'); + const router = new VueRouter({ routes: [ - { path: '/', component: Landing }, - { path: '/c/:codeParam', component: Landing, props: true }, - { path: '/u/:codeParam', component: Upload, props: true } + { path: '/', component: () => import('./route/Landing.vue'), + children: [ + { path: 'c/:codeParam', component: Code, props: true }, + { path: 'u/:codeParam', component: () => import('./route/Upload.vue'), props: true }, + { path: '', component: Code } + ] + }, + { path: '/admin', component: () => import('./route/admin/Landing.vue'), + children: [ + { 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: '*', redirect: '/' } ] }); diff --git a/public/src/route/Code.vue b/public/src/route/Code.vue new file mode 100644 index 0000000..a368f22 --- /dev/null +++ b/public/src/route/Code.vue @@ -0,0 +1,92 @@ + + + + + \ No newline at end of file diff --git a/public/src/route/Landing.vue b/public/src/route/Landing.vue index 4fca8e9..faa9367 100644 --- a/public/src/route/Landing.vue +++ b/public/src/route/Landing.vue @@ -1,94 +1,20 @@ - - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/public/src/route/admin/Codes.vue b/public/src/route/admin/Codes.vue new file mode 100644 index 0000000..188fab2 --- /dev/null +++ b/public/src/route/admin/Codes.vue @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/public/src/route/admin/Landing.vue b/public/src/route/admin/Landing.vue new file mode 100644 index 0000000..080923b --- /dev/null +++ b/public/src/route/admin/Landing.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/public/src/route/admin/Login.vue b/public/src/route/admin/Login.vue new file mode 100644 index 0000000..31f9022 --- /dev/null +++ b/public/src/route/admin/Login.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/public/src/route/admin/Profile.vue b/public/src/route/admin/Profile.vue new file mode 100644 index 0000000..188fab2 --- /dev/null +++ b/public/src/route/admin/Profile.vue @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/public/src/route/admin/Users.vue b/public/src/route/admin/Users.vue new file mode 100644 index 0000000..188fab2 --- /dev/null +++ b/public/src/route/admin/Users.vue @@ -0,0 +1,4 @@ + \ No newline at end of file