diff --git a/.gitignore b/.gitignore index a39634b..6025c2a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ data custom/*.js public/dist config.js -*.sublime-workspace \ No newline at end of file +*.sublime-workspace +/npm-debug.log \ No newline at end of file diff --git a/index.js b/index.js index 5ffed89..503223f 100644 --- a/index.js +++ b/index.js @@ -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)); })(); \ No newline at end of file diff --git a/lib/api/admin.js b/lib/api/admin.js new file mode 100644 index 0000000..d224df6 --- /dev/null +++ b/lib/api/admin.js @@ -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; +} \ No newline at end of file diff --git a/lib/api/token.js b/lib/api/token.js index 05f2a29..0c13a7d 100644 --- a/lib/api/token.js +++ b/lib/api/token.js @@ -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; } \ No newline at end of file diff --git a/lib/api/upload.js b/lib/api/upload.js index 29f57c6..1ab90ef 100644 --- a/lib/api/upload.js +++ b/lib/api/upload.js @@ -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; } \ No newline at end of file diff --git a/lib/authtokens.js b/lib/authtokens.js new file mode 100644 index 0000000..3df0234 --- /dev/null +++ b/lib/authtokens.js @@ -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; \ No newline at end of file diff --git a/lib/repository/codes.js b/lib/repository/code.js similarity index 60% rename from lib/repository/codes.js rename to lib/repository/code.js index 03ba15a..84a90dd 100644 --- a/lib/repository/codes.js +++ b/lib/repository/code.js @@ -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; \ No newline at end of file +module.exports = { + Code, + CodeRepository +} \ No newline at end of file diff --git a/lib/repository/index.js b/lib/repository/index.js index 7872d85..0359a51 100644 --- a/lib/repository/index.js +++ b/lib/repository/index.js @@ -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); }); }); }); } diff --git a/lib/repository/uploads.js b/lib/repository/upload.js similarity index 85% rename from lib/repository/uploads.js rename to lib/repository/upload.js index 13cea84..421d917 100644 --- a/lib/repository/uploads.js +++ b/lib/repository/upload.js @@ -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; \ No newline at end of file +module.exports = { + UploadRepository +} \ No newline at end of file diff --git a/lib/repository/user.js b/lib/repository/user.js new file mode 100644 index 0000000..5d95f83 --- /dev/null +++ b/lib/repository/user.js @@ -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 +} \ No newline at end of file diff --git a/lib/repository/users.js b/lib/repository/users.js deleted file mode 100644 index 2800475..0000000 --- a/lib/repository/users.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/package.json b/package.json index eb6ed2c..8659b9d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/src/route/admin/Login.vue b/public/src/route/admin/Login.vue index 31f9022..a96b3fb 100644 --- a/public/src/route/admin/Login.vue +++ b/public/src/route/admin/Login.vue @@ -1,10 +1,33 @@ \ No newline at end of file