Restructured in preparation for admin panel

This commit is contained in:
Mark van Renswoude 2018-03-21 16:51:46 +01:00
parent 1febe41516
commit a07c9a3ef7
17 changed files with 593 additions and 390 deletions

View File

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

32
lib/api/token.js Normal file
View File

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

60
lib/api/upload.js Normal file
View File

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

View File

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

82
lib/repository/codes.js Normal file
View File

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

61
lib/repository/index.js Normal file
View File

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

70
lib/repository/uploads.js Normal file
View File

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

104
lib/repository/users.js Normal file
View File

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

View File

@ -7,17 +7,7 @@
</div>
</div>
<div class="content">
<div class="logo">
<img src="/images/logo.png">
</div>
<router-view></router-view>
</div>
<div class="disclaimer">
{{ $t('disclaimer') }}
</div>
<router-view></router-view>
</div>
</div>
</template>

View File

@ -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: '/' }
]
});

92
public/src/route/Code.vue Normal file
View File

@ -0,0 +1,92 @@
<template>
<div id="landing">
<form class="pure-form pure-form-stacked" @submit.prevent="checkCode">
<fieldset class="pure-group">
<input type="text" class="pure-input-1-2" v-model="code.value" :placeholder="$t('landing.invitePlaceholder')">
</fieldset>
<button type="submit" class="pure-button pure-button-primary" :disabled="code.value.trim() == '' || code.checking">{{ $t(code.checking ? 'landing.inviteButtonChecking' : 'landing.inviteButton') }} <span v-if="code.checking"><i class="fas fa-spinner fa-pulse"></i></span></button>
</form>
</div>
</template>
<script>
import axios from 'axios';
import shared from '../shared';
export default {
props: [
'codeParam'
],
data () {
return {
code: {
value: '',
checking: false
}
}
},
created()
{
var self = this;
if (self.codeParam)
{
self.code.value = self.codeParam;
self.checkCode();
}
},
methods: {
checkCode()
{
var self = this;
if (self.code.checking)
return;
self.code.checking = true;
axios.post('/token/upload', {
code: self.code.value
})
.then((response) =>
{
shared.token = response.data;
self.$router.push({ path: '/u/' + self.code.value });
})
.catch((error) =>
{
if (error.response && error.response.status == 403)
shared.$emit('showNotification', self.$i18n.t('notification.invalidCode'), false);
else
shared.$emit('showNotification', error.message);
})
.then(() =>
{
self.code.checking = false;
});
}
}
}
</script>
<style lang="scss">
@import '../../../node_modules/uppy/src/scss/uppy.scss';
#landing
{
text-align: center;
input
{
display: inline-block;
}
.login
{
margin-top: 5em;
}
}
</style>

View File

@ -1,94 +1,20 @@
<template>
<div id="landing">
<form class="pure-form pure-form-stacked" @submit.prevent="checkCode">
<fieldset class="pure-group">
<input type="text" class="pure-input-1-2" v-model="code.value" :placeholder="$t('landing.invitePlaceholder')">
</fieldset>
<button type="submit" class="pure-button pure-button-primary" :disabled="code.value.trim() == '' || code.checking">{{ $t(code.checking ? 'landing.inviteButtonChecking' : 'landing.inviteButton') }} <span v-if="code.checking"><i class="fas fa-spinner fa-pulse"></i></span></button>
</form>
</div>
</template>
<script>
import axios from 'axios';
import shared from '../shared';
export default {
name: 'app',
props: [
'codeParam'
],
data () {
return {
code: {
value: '',
checking: false
}
}
},
created()
{
var self = this;
if (self.codeParam)
{
self.code.value = self.codeParam;
self.checkCode();
}
},
methods: {
checkCode()
{
var self = this;
if (self.code.checking)
return;
self.code.checking = true;
axios.post('/token/upload', {
code: self.code.value
})
.then((response) =>
{
shared.token = response.data;
self.$router.push({ path: '/u/' + self.code.value });
})
.catch((error) =>
{
if (error.response && error.response.status == 403)
shared.$emit('showNotification', self.$i18n.t('notification.invalidCode'), false);
else
shared.$emit('showNotification', error.message);
})
.then(() =>
{
self.code.checking = false;
});
}
}
}
</script>
<style lang="scss">
@import '../../../node_modules/uppy/src/scss/uppy.scss';
#landing
{
text-align: center;
input
{
display: inline-block;
}
.login
{
margin-top: 5em;
}
}
</style>
<template>
<div id="landing">
<div class="content">
<div class="logo">
<img src="/images/logo.png">
</div>
<router-view></router-view>
</div>
<div class="disclaimer">
{{ $t('disclaimer') }}
</div>
</div>
</template>
<script>
export default {
}
</script>

View File

@ -0,0 +1,4 @@
<script>
export default {
}
</script>

View File

@ -0,0 +1,10 @@
<template>
<div id="admin">
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>

View File

@ -0,0 +1,10 @@
<template>
<div id="login">
Login.
</div>
</template>
<script>
export default {
}
</script>

View File

@ -0,0 +1,4 @@
<script>
export default {
}
</script>

View File

@ -0,0 +1,4 @@
<script>
export default {
}
</script>