Further admin preparations

Improved exception handling in async functions
This commit is contained in:
Mark van Renswoude 2018-03-22 16:45:11 +01:00
parent a07c9a3ef7
commit 8b9ed0e666
13 changed files with 407 additions and 192 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ data
custom/*.js
public/dist
config.js
*.sublime-workspace
*.sublime-workspace
/npm-debug.log

View File

@ -16,69 +16,62 @@ const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackConfig = require('./webpack.config.js');
(async function()
(async () =>
{
const isDevelopment = process.env.NODE_ENV !== 'production';
try
{
const isDevelopment = process.env.NODE_ENV !== 'production';
const repository = new Repository(config.database);
await repository.load()
.catch((err) =>
{
console.log(err);
const repository = new Repository(config.database);
await repository.load();
const tusServer = new tus.Server();
tusServer.datastore = new tus.FileStore({
path: config.fileUpload.url,
directory: config.fileUpload.path
});
const app = express();
const tusServer = new tus.Server();
tusServer.datastore = new tus.FileStore({
path: config.fileUpload.url,
directory: config.fileUpload.path
});
app.disable('x-powered-by');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const registerAPI = (name) => { require('./lib/api/' + name)(app, repository) }
const loadAPI = (route, name) => { app.use(route, require('./lib/api/' + name)(repository)) }
fs.readdir('./lib/api', (err, files) =>
{
if (err)
loadAPI('/', 'upload');
loadAPI('/token', 'token');
loadAPI('/admin', 'admin');
// Frontend
if (isDevelopment)
{
console.log(err);
return;
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
}));
app.use(webpackHotMiddleware(compiler));
}
_.forEach(files, (fileName) =>
{
console.log('Loading API ' + fileName + '...');
registerAPI(fileName);
});
});
app.use(express.static(path.join(__dirname, 'public', 'dist')));
// Frontend
if (isDevelopment)
{
const compiler = webpack(webpackConfig);
// 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/') });
app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
}));
app.use(webpackHotMiddleware(compiler));
var server = app.listen(config.port, () => console.log('Recv running on port ' + server.address().port));
}
catch (e)
{
console.log(e);
process.exit(1);
}
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));
})();

54
lib/api/admin.js Normal file
View File

@ -0,0 +1,54 @@
const config = require('../../config');
const express = require('Express');
const asyncHandler = require('express-async-handler');
const jwt = require('jsonwebtoken');
async 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, async (err, decoded) =>
{
try
{
if (err)
{
res.sendStatus(403);
return;
}
if (decoded.userId)
await onVerified(decoded);
else
res.sendStatus(400);
}
catch (e)
{
console.log(e);
res.sendStatus(500);
}
});
}
module.exports = (repository) =>
{
var router = express.Router();
router.get('/codes', asyncHandler(async (req, res) =>
{
await checkAuthorization(req, res, async (decoded) =>
{
res.send(await repository.codes.getCodes(decoded.userId));
});
}));
return router;
}

View File

@ -1,10 +1,14 @@
const config = require('../../config');
const express = require('Express');
const asyncHandler = require('express-async-handler');
const jwt = require('jsonwebtoken');
module.exports = (app, repository) =>
module.exports = (repository) =>
{
app.post('/token/upload', async (req, res) =>
var router = express.Router();
router.post('/upload', asyncHandler(async (req, res) =>
{
if (!req.body.code)
{
@ -17,7 +21,7 @@ module.exports = (app, repository) =>
{
jwt.sign({
code: req.body.code,
userId: userId
codeUserId: userId
}, config.jwtSecret, (err, token) =>
{
if (err)
@ -28,5 +32,33 @@ module.exports = (app, repository) =>
}
else
res.sendStatus(403);
});
}));
router.post('/login', asyncHandler(async (req, res) =>
{
if (!req.body.username || !req.body.password)
{
res.sendStatus(400);
return;
}
var user = await repository.users.getLoginUser(req.body.username, req.body.password);
if (user !== null)
{
jwt.sign({
userId: user.id
}, config.jwtSecret, (err, token) =>
{
if (err)
res.sendStatus(500);
else
res.send(token);
});
}
else
res.sendStatus(403);
}));
return router;
}

View File

@ -1,9 +1,10 @@
const config = require('../../config');
const express = require('express');
const asyncHandler = require('express-async-handler');
const jwt = require('jsonwebtoken');
function checkAuthorization(req, res, onVerified)
async function checkAuthorization(req, res, onVerified)
{
if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer')
{
@ -12,24 +13,37 @@ function checkAuthorization(req, res, onVerified)
}
var token = req.headers.authorization.split(' ')[1];
jwt.verify(token, config.jwtSecret, (err, decoded) =>
jwt.verify(token, config.jwtSecret, async (err, decoded) =>
{
if (err)
try
{
res.sendStatus(403);
return;
}
if (err)
{
res.sendStatus(403);
return;
}
onVerified(decoded);
if (decoded.codeUserId)
await onVerified(decoded);
else
res.sendStatus(400);
}
catch (e)
{
console.log(e);
res.sendStatus(500);
}
});
}
module.exports = (app, repository) =>
module.exports = (repository) =>
{
var router = express.Router();
// Upload API
app.post('/complete', (req, res) =>
router.post('/complete', asyncHandler(async (req, res) =>
{
if (!req.body.files)
{
@ -37,24 +51,25 @@ module.exports = (app, repository) =>
return;
}
checkAuthorization(req, res, async (decoded) =>
await checkAuthorization(req, res, async (decoded) =>
{
var expiration = null; // TODO set expiration properties
var uploadId = await repository.uploads.addUpload(decoded.userId, req.body.files, expiration);
var uploadId = await repository.uploads.addUpload(decoded.codeUserId, req.body.files, expiration);
res.send({ id: uploadId });
});
});
}));
// Tus upload
const uploadApp = express();
uploadApp.all('*', (req, res) =>
uploadApp.all('*', asyncHandler(async (req, res) =>
{
checkAuthorization(req, res, (decoded) =>
await checkAuthorization(req, res, async (decoded) =>
{
tusServer.handle(req, res);
});
});
}));
app.use('/upload', uploadApp);
router.use('/upload', uploadApp);
return router;
}

13
lib/authtokens.js Normal file
View File

@ -0,0 +1,13 @@
const AuthTokens = {
ManageUsers: 'manageUsers'
}
var all = [];
for (var key in AuthTokens)
{
if (AuthTokens.hasOwnProperty(key))
all.push(AuthTokens[key]);
}
AuthTokens.all = all;
module.exports = AuthTokens;

View File

@ -1,4 +1,21 @@
class CodesRepository
const _ = require('lodash');
class Code
{
constructor(values)
{
var self = this;
self.id = values.id || values._id || null;
self.userId = values.userId || null;
self.created = values.created || new Date();
self.expiration = values.expiration || null;
}
}
class CodeRepository
{
constructor(store)
{
@ -7,7 +24,7 @@ class CodesRepository
}
async init()
init()
{
var self = this;
@ -31,7 +48,7 @@ class CodesRepository
}
async findCodeUserId(code)
findCodeUserId(code)
{
var self = this;
@ -76,7 +93,28 @@ class CodesRepository
maxTimeout: 0
});
}
getCodes(userId)
{
return new Promise((resolve, reject) =>
{
self.store.find({ userId: userId }, (err, docs) =>
{
if (err)
{
reject(err);
return;
}
resolve(_.map(docs, (doc) => new Code(doc)));
});
});
}
}
module.exports = CodesRepository;
module.exports = {
Code,
CodeRepository
}

View File

@ -2,14 +2,13 @@
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');
const UserRepository = require('./user').UserRepository;
const CodeRepository = require('./code').CodeRepository;
const UploadRepository = require('./upload').UploadRepository;
class Repository
@ -21,7 +20,7 @@ class Repository
}
async load()
load()
{
var self = this;
@ -38,7 +37,7 @@ class Repository
return store;
};
mkdirp(self.config.path, async (err) =>
mkdirp(self.config.path, (err) =>
{
if (err)
{
@ -46,12 +45,13 @@ class Repository
return;
}
self.users = new UsersRepository(initStore('users.db'));
self.codes = new CodesRepository(initStore('codes.db'));
self.uploads = new UploadsRepository(initStore('uploads.db'));
self.users = new UserRepository(initStore('user.db'));
self.codes = new CodeRepository(initStore('code.db'));
self.uploads = new UploadRepository(initStore('upload.db'));
await self.users.init();
resolve();
self.users.init()
.then(() => { resolve() })
.catch((e) => { reject(e); });
});
});
}

View File

@ -1,4 +1,4 @@
class UploadsRepository
class UploadRepository
{
constructor(store)
{
@ -7,7 +7,7 @@ class UploadsRepository
}
async init()
init()
{
var self = this;
@ -31,7 +31,7 @@ class UploadsRepository
}
async addUpload(userId, files, expiration)
addUpload(userId, files, expiration)
{
var self = this;
@ -67,4 +67,6 @@ class UploadsRepository
}
module.exports = UploadsRepository;
module.exports = {
UploadRepository
}

147
lib/repository/user.js Normal file
View File

@ -0,0 +1,147 @@
const AuthTokens = require('../authtokens');
const bcrypt = require('bcrypt');
class User
{
constructor(values)
{
var self = this;
self.id = values.id || values._id || null;
self.username = values.username || null;
self.password = values.password || null;
self.email = values.email || null;
self.auth = values.auth || [];
if (values.hasOwnProperty('active'))
self.active = (values.active === true);
else
self.active = true;
self.created = values.created || new Date();
self.createdByUserId = values.createdByUserId || null;
}
hasAuth(token)
{
return self.auth.includes(token);
}
}
class UserRepository
{
constructor(store)
{
var self = this;
self.store = store;
}
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(new User({
username: 'admin',
password: 'changeme',
auth: AuthTokens.all
}))
.then(() => { resolve() })
.catch((e) => { reject(e); });
}
else
resolve();
});
});
}
getLoginUser(username, password)
{
var self = this;
return new Promise((resolve, reject) =>
{
self.store.findOne({ username: username, active: true }, (err, doc) =>
{
if (err)
{
reject(err);
return;
}
if (doc == null)
{
resolve(false);
return;
}
bcrypt.compare(password, doc.hashedPassword, (err, res) =>
{
if (err)
reject(err)
else
resolve(res ? new User(doc) : null);
});
});
});
}
addUser(user)
{
var self = this;
return new Promise((resolve, reject) =>
{
bcrypt.hash(user.password, 10, function(err, hash)
{
if (err)
{
reject(err);
return;
}
self.store.insert({
username: user.username,
email: user.email,
hashedPassword: hash,
created: user.created,
createdByUserId: user.createdByUserId,
active: user.active,
auth: user.auth
}, (err, dbUser) =>
{
if (err)
{
reject(err);
return;
}
resolve(dbUser._id);
});
});
});
}
}
module.exports = {
User,
UserRepository
}

View File

@ -1,104 +0,0 @@
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

@ -20,6 +20,7 @@
"body-parser": "^1.18.2",
"debug": "^3.1.0",
"express": "^4.16.2",
"express-async-handler": "^1.1.2",
"jsonwebtoken": "^8.1.1",
"lodash": "^4.17.5",
"mkdirp": "^0.5.1",

View File

@ -1,10 +1,33 @@
<template>
<div id="login">
Login.
<form class="pure-form pure-form-stacked" @submit.prevent="login">
<fieldset class="pure-group">
<input type="text" class="pure-input-1-2" v-model="username" :placeholder="$t('landing.invitePlaceholder')">
<input type="password" class="pure-input-1-2" v-model="password" :placeholder="$t('landing.invitePlaceholder')">
</fieldset>
<button type="submit" class="pure-button pure-button-primary" :disabled="username.trim() == '' || password.trim() == '' || checking">{{ $t(checking ? 'landing.inviteButtonChecking' : 'landing.inviteButton') }} <span v-if="checking"><i class="fas fa-spinner fa-pulse"></i></span></button>
</form>
</div>
</template>
<script>
export default {
data: () =>
{
return {
username: '',
password: '',
checking: false
};
},
methods: {
login()
{
alert('TODO');
}
}
}
</script>