From 8b1b501bbdf70eb988d80bb4d8a5d67873f9d7b0 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Wed, 30 Aug 2017 22:45:48 -0700 Subject: [PATCH] Start working on /account/data controller --- integration_test/db/account.js | 5 ++ integration_test/db/channel.js | 5 ++ package.json | 5 +- src/db/account.js | 6 +- src/db/channel.js | 5 +- src/errors.js | 3 +- src/server.js | 9 ++- src/web/routes/account/data.js | 141 +++++++++++++++++++++++++++++++++ src/web/webserver.js | 13 ++- 9 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 src/web/routes/account/data.js diff --git a/integration_test/db/account.js b/integration_test/db/account.js index a9e6729c..df0627aa 100644 --- a/integration_test/db/account.js +++ b/integration_test/db/account.js @@ -1,6 +1,7 @@ const assert = require('assert'); const AccountDB = require('../../lib/db/account').AccountDB; const testDB = require('../testutil/db').testDB; +const { InvalidRequestError } = require('../../lib/errors'); const accountDB = new AccountDB(testDB); @@ -142,6 +143,10 @@ describe('AccountDB', () => { ).then(() => { throw new Error('Expected failure due to missing user'); }).catch(error => { + assert( + error instanceof InvalidRequestError, + 'Expected InvalidRequestError' + ); assert.strictEqual( error.message, 'Cannot update: name "test" does not exist' diff --git a/integration_test/db/channel.js b/integration_test/db/channel.js index 313658c1..fa9ead97 100644 --- a/integration_test/db/channel.js +++ b/integration_test/db/channel.js @@ -1,6 +1,7 @@ const assert = require('assert'); const ChannelDB = require('../../lib/db/channel').ChannelDB; const testDB = require('../testutil/db').testDB; +const { InvalidRequestError } = require('../../lib/errors'); const channelDB = new ChannelDB(testDB); @@ -130,6 +131,10 @@ describe('ChannelDB', () => { }).then(() => { throw new Error('Expected error due to already existing channel'); }).catch(error => { + assert( + error instanceof InvalidRequestError, + 'Expected InvalidRequestError' + ); assert.strictEqual( error.message, 'Channel "i_test" is already registered.' diff --git a/package.json b/package.json index 43831bf4..b5c60b60 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "license": "MIT", "dependencies": { + "@calzoneman/express-babel-decorators": "^1.0.0", "@calzoneman/jsli": "^2.0.1", "bcrypt": "^0.8.5", "bluebird": "^2.10.1", @@ -59,6 +60,7 @@ "babel-core": "^6.25.0", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-preset-env": "^1.5.2", "coffee-script": "^1.9.2", @@ -80,7 +82,8 @@ "plugins": [ "transform-async-to-generator", "add-module-exports", - "transform-flow-strip-types" + "transform-flow-strip-types", + "transform-decorators-legacy" ] } } diff --git a/src/db/account.js b/src/db/account.js index 1945acb8..3040f0e1 100644 --- a/src/db/account.js +++ b/src/db/account.js @@ -1,3 +1,5 @@ +import { InvalidRequestError } from '../errors'; + const LOGGER = require('@calzoneman/jsli')('AccountDB'); class AccountDB { @@ -26,7 +28,9 @@ class AccountDB { .where({ name }); if (rowsUpdated === 0) { - throw new Error(`Cannot update: name "${name}" does not exist`); + throw new InvalidRequestError( + `Cannot update: name "${name}" does not exist` + ); } }); } diff --git a/src/db/channel.js b/src/db/channel.js index 4662ab32..e7ba028b 100644 --- a/src/db/channel.js +++ b/src/db/channel.js @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import Promise from 'bluebird'; +import { InvalidRequestError } from '../errors'; const unlinkAsync = Promise.promisify(fs.unlink); @@ -41,7 +42,9 @@ class ChannelDB { .first(); if (existing) { - throw new Error(`Channel "${name}" is already registered.`); + throw new InvalidRequestError( + `Channel "${name}" is already registered.` + ); } await tx.table('channels') diff --git a/src/errors.js b/src/errors.js index bd33cb82..7b4160bd 100644 --- a/src/errors.js +++ b/src/errors.js @@ -7,4 +7,5 @@ export const CSRFError = createError('CSRFError'); export const HTTPError = createError('HTTPError', { status: HTTPStatus.INTERNAL_SERVER_ERROR }); -export const ValidationError = createError('ValidationError'); \ No newline at end of file +export const ValidationError = createError('ValidationError'); +export const InvalidRequestError = createError('InvalidRequestError'); diff --git a/src/server.js b/src/server.js index 5392efff..4efc0e2d 100644 --- a/src/server.js +++ b/src/server.js @@ -52,6 +52,8 @@ import { LegacyModule } from './legacymodule'; import { PartitionModule } from './partition/partitionmodule'; import * as Switches from './switches'; import { Gauge } from 'prom-client'; +import { AccountDB } from './db/account'; +import { ChannelDB } from './db/channel'; var Server = function () { var self = this; @@ -83,6 +85,9 @@ var Server = function () { self.db.init(); ChannelStore.init(); + const accountDB = new AccountDB(db.getDB()); + const channelDB = new ChannelDB(db.getDB()); + // webserver init ----------------------------------------------------- const ioConfig = IOConfiguration.fromOldConfig(Config); const webConfig = WebConfiguration.fromOldConfig(Config); @@ -102,7 +107,9 @@ var Server = function () { clusterClient, channelIndex, session, - globalMessageBus); + globalMessageBus, + accountDB, + channelDB); // http/https/sio server init ----------------------------------------- var key = "", cert = "", ca = undefined; diff --git a/src/web/routes/account/data.js b/src/web/routes/account/data.js new file mode 100644 index 00000000..384b95a6 --- /dev/null +++ b/src/web/routes/account/data.js @@ -0,0 +1,141 @@ +import { GET, POST, PATCH, DELETE } from '@calzoneman/express-babel-decorators'; +import { CSRFError, InvalidRequestError } from '../../../errors'; +import { verify as csrfVerify } from '../../csrf'; + +const LOGGER = require('@calzoneman/jsli')('AccountDataRoute'); + +function checkAcceptsJSON(req, res) { + if (!req.accepts('application/json')) { + res.status(406).send('Not Acceptable'); + + return false; + } + + return true; +} + +async function authorize(req, res) { + if (!req.signedCookies || !req.signedCookies.auth) { + res.status(401).json({ + error: 'Authorization required' + }); + + return false; + } + + try { + csrfVerify(req); + } catch (error) { + if (error instanceof CSRFError) { + res.status(403).json({ + error: 'Invalid CSRF token' + }); + } else { + LOGGER.error('CSRF check failed: %s', error.stack); + res.status(503).json({ error: 'Internal error' }); + } + + return false; + } + + // TODO: verify session + + return true; +} + +function reportError(req, res, error) { + if (error instanceof InvalidRequestError) { + res.status(400).json({ error: error.message }); + } else { + LOGGER.error( + '%s %s: %s', + req.method, + req.originalUrl, + error.stack + ); + res.status(503).json({ error: 'Internal error' }); + } + +} + +class AccountDataRoute { + constructor(accountDB, channelDB) { + this.accountDB = accountDB; + this.channelDB = channelDB; + } + + @GET('/account/data/:user') + async getAccount(req, res) { + if (!checkAcceptsJSON(req, res)) return; + if (!await authorize(req, res)) return; + + try { + const user = await this.accountDB.getByName(req.params.user); + + if (user) { + // Whitelist fields to expose, to avoid accidental + // information leaks when new fields are added. + const result = { + name: user.name, + email: user.email, + profile: user.profile, + time: user.time + }; + + res.status(200).json({ result }); + } else { + res.status(404).json({ result: null }); + } + } catch (error) { + reportError(req, res, error); + } + } + + @PATCH('/account/data/:user') + async updateAccount(req, res) { + if (!checkAcceptsJSON(req, res)) return; + if (!await authorize(req, res)) return; + + res.status(501).json({ error: 'Not implemented' }); + } + + @GET('/account/data/:user/channels') + async listChannels(req, res) { + if (!checkAcceptsJSON(req, res)) return; + if (!await authorize(req, res)) return; + + try { + const channels = await this.channelDB.listByOwner(req.params.user).map( + channel => ({ + name: channel.name, + owner: channel.owner, + time: channel.time, + last_loaded: channel.last_loaded, + owner_last_seen: channel.owner_last_seen + }) + ); + + res.status(200).json({ result: channels }); + } catch (error) { + reportError(req, res, error); + } + } + + @POST('/account/data/:user/channels/:name') + async createChannel(req, res) { + if (!checkAcceptsJSON(req, res)) return; + if (!await authorize(req, res)) return; + + res.status(501).json({ error: 'Not implemented' }); + } + + @DELETE('/account/data/:user/channels/:name') + async deleteChannel(req, res) { + if (!checkAcceptsJSON(req, res)) return; + if (!await authorize(req, res)) return; + + res.status(501).json({ error: 'Not implemented' }); + } +} + +export { AccountDataRoute }; diff --git a/src/web/webserver.js b/src/web/webserver.js index 0c03f88d..90a20688 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -191,7 +191,9 @@ module.exports = { clusterClient, channelIndex, session, - globalMessageBus + globalMessageBus, + accountDB, + channelDB ) { patchExpressToHandleAsync(); const chanPath = Config.get('channel-path'); @@ -253,6 +255,15 @@ module.exports = { require('../google2vtt').attach(app); require('./routes/google_drive_userscript')(app); require('./routes/ustream_bypass')(app); + + /* + const { AccountDataRoute } = require('./routes/account/data'); + require('@calzoneman/express-babel-decorators').bind( + app, + new AccountDataRoute(accountDB, channelDB) + ); + */ + app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), { maxAge: webConfig.getCacheTTL() }));