diff --git a/src/controller/account.js b/src/controller/account.js new file mode 100644 index 00000000..b983ad46 --- /dev/null +++ b/src/controller/account.js @@ -0,0 +1,70 @@ +import { InvalidRequestError } from '../errors'; +import { isValidEmail } from '../utilities'; +import { parse as parseURL } from 'url'; +import bcrypt from 'bcrypt'; +import Promise from 'bluebird'; + +Promise.promisifyAll(bcrypt); + +class AccountController { + constructor(accountDB, globalMessageBus) { + this.accountDB = accountDB; + this.globalMessageBus = globalMessageBus; + } + + async getAccount(name) { + const user = await this.accountDB.getByName(name); + + if (user) { + return { + name: user.name, + email: user.email, + profile: user.profile, + time: user.time + }; + } else { + return null; + } + } + + async updateAccount(name, updates, password = null) { + let requirePassword = false; + const fields = {}; + + if (!updates || updates.toString() !== '[object Object]') { + throw new InvalidRequestError('Malformed input'); + } + + if (updates.email) { + if (!isValidEmail(updates.email)) { + throw new InvalidRequestError('Invalid email address'); + } + + fields.email = updates.email; + requirePassword = true; + } + + if (requirePassword) { + if (!password) { + throw new InvalidRequestError('Password required'); + } + + const user = await this.accountDB.getUserByName(name); + + if (!user) { + throw new InvalidRequestError('User does not exist'); + } + + // For legacy reasons, the password was truncated to 100 chars. + password = password.substring(0, 100); + + if (!await bcrypt.compareAsync(password, user.password)) { + throw new InvalidRequestError('Invalid password'); + } + } + + await this.accountDB.updateByName(name, fields); + } +} + +export { AccountController }; diff --git a/src/server.js b/src/server.js index 4efc0e2d..c4c26532 100644 --- a/src/server.js +++ b/src/server.js @@ -54,6 +54,7 @@ import * as Switches from './switches'; import { Gauge } from 'prom-client'; import { AccountDB } from './db/account'; import { ChannelDB } from './db/channel'; +import { AccountController } from './controller/account'; var Server = function () { var self = this; @@ -88,6 +89,8 @@ var Server = function () { const accountDB = new AccountDB(db.getDB()); const channelDB = new ChannelDB(db.getDB()); + const accountController = new AccountController(accountDB, globalMessageBus); + // webserver init ----------------------------------------------------- const ioConfig = IOConfiguration.fromOldConfig(Config); const webConfig = WebConfiguration.fromOldConfig(Config); @@ -108,7 +111,7 @@ var Server = function () { channelIndex, session, globalMessageBus, - accountDB, + accountController, channelDB); // http/https/sio server init ----------------------------------------- diff --git a/src/utilities.js b/src/utilities.js index 566d4f87..3d9bb3d2 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -53,6 +53,10 @@ }, root.isValidEmail = function (email) { + if (typeof email !== "string") { + return false; + } + if (email.length > 255) { return false; } diff --git a/src/web/routes/account/data.js b/src/web/routes/account/data.js index 924778e9..b10881c4 100644 --- a/src/web/routes/account/data.js +++ b/src/web/routes/account/data.js @@ -75,8 +75,8 @@ function reportError(req, res, error) { } class AccountDataRoute { - constructor(accountDB, channelDB, csrfVerify, verifySessionAsync) { - this.accountDB = accountDB; + constructor(accountController, channelDB, csrfVerify, verifySessionAsync) { + this.accountController = accountController; this.channelDB = channelDB; this.csrfVerify = csrfVerify; this.verifySessionAsync = verifySessionAsync; @@ -88,22 +88,9 @@ class AccountDataRoute { if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; try { - const user = await this.accountDB.getByName(req.params.user); + const user = await this.accountController.getAccount(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 }); - } + res.status(user === null ? 404 : 200).json({ result: user }); } catch (error) { reportError(req, res, error); } @@ -114,7 +101,14 @@ class AccountDataRoute { if (!checkAcceptsJSON(req, res)) return; if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - res.status(501).json({ error: 'Not implemented' }); + const { password, updates } = req.body; + + try { + this.accountController.updateAccount(req.user, updates, password); + res.status(204).send(); + } catch (error) { + reportError(req, res, error); + } } @GET('/account/data/:user/channels') diff --git a/src/web/webserver.js b/src/web/webserver.js index 72bc6c83..591fadce 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -193,7 +193,7 @@ module.exports = { channelIndex, session, globalMessageBus, - accountDB, + accountController, channelDB ) { patchExpressToHandleAsync(); @@ -209,6 +209,9 @@ module.exports = { extended: false, limit: '1kb' // No POST data should ever exceed this size under normal usage })); + app.use(bodyParser.json({ + limit: '1kb' + })); if (webConfig.getCookieSecret() === 'change-me') { LOGGER.warn('The configured cookie secret was left as the ' + 'default of "change-me".'); @@ -261,7 +264,12 @@ module.exports = { const { AccountDataRoute } = require('./routes/account/data'); require('@calzoneman/express-babel-decorators').bind( app, - new AccountDataRoute(accountDB, channelDB, csrfVerify, verifySessionAsync) + new AccountDataRoute( + accountController, + channelDB, + csrfVerify, + verifySessionAsync + ) ); } diff --git a/test/web/routes/account/data.js b/test/web/routes/account/data.js index b6dbcf73..a8489455 100644 --- a/test/web/routes/account/data.js +++ b/test/web/routes/account/data.js @@ -3,6 +3,7 @@ const sinon = require('sinon'); const express = require('express'); const { AccountDB } = require('../../../../lib/db/account'); const { ChannelDB } = require('../../../../lib/db/channel'); +const { AccountController } = require('../../../../lib/controller/account'); const { AccountDataRoute } = require('../../../../lib/web/routes/account/data'); const http = require('http'); const expressBabelDecorators = require('@calzoneman/express-babel-decorators'); @@ -10,6 +11,7 @@ const nodeurl = require('url'); const Promise = require('bluebird'); const bodyParser = require('body-parser'); const { CSRFError } = require('../../../../lib/errors'); +const { EventEmitter } = require('events'); const TEST_PORT = 10111; const URL_BASE = `http://localhost:${TEST_PORT}`; @@ -89,7 +91,7 @@ describe('AccountDataRoute', () => { })); accountDataRoute = new AccountDataRoute( - realAccountDB, + new AccountController(realAccountDB, new EventEmitter()), realChannelDB, csrfVerify, verifySessionAsync