From b59161a9f23acc4418d9c1564b8e1dc22fc9401f Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Sat, 3 Dec 2016 20:10:33 +0100 Subject: [PATCH] fix(session): update tests (karma), and routes This commit changes the default login/logout routes to use the `/auth` prefix. All tests and associated permissions have been updated. We now have three route: 1) `/auth/login` 2) `/auth/logout` 3) `/auth/reload` The `reload` route will send back the logged in user's updated session information. This is particularly useful when the Enterprise, Project, User or Permissions services detect changes to their bound items. --- client/src/js/components/bhNavigation.js | 25 +- client/src/js/services/Session.js | 31 +- client/src/js/services/UserService.js | 2 +- client/src/js/services/tree.js | 2 +- server/config/express.js | 2 +- server/config/routes.js | 8 +- server/controllers/auth.js | 430 ++++++++---------- server/lib/topic.js | 1 + .../services/SessionService.spec.js | 11 +- test/integration/login.js | 10 +- test/integration/setup.js | 2 +- 11 files changed, 248 insertions(+), 276 deletions(-) diff --git a/client/src/js/components/bhNavigation.js b/client/src/js/components/bhNavigation.js index be0acc27cc..7b1a721e21 100644 --- a/client/src/js/components/bhNavigation.js +++ b/client/src/js/components/bhNavigation.js @@ -26,19 +26,21 @@ function NavigationController($location, $rootScope, Tree, AppCache, Notify) { */ var unitsIndex = { id : {}, path : {} }; - Tree.units() - .then(function (units) { + function loadTreeUnits() { + Tree.units() + .then(function (units) { - Tree.sortByTranslationKey(units); - $ctrl.units = units; + Tree.sortByTranslationKey(units); + $ctrl.units = units; - calculateUnitIndex($ctrl.units); - expandInitialUnits($ctrl.units); + calculateUnitIndex($ctrl.units); + expandInitialUnits($ctrl.units); - // updates the tree selection on path change - updateSelectionOnPathChange(); - }) - .catch(Notify.handleError); + // updates the tree selection on path change + updateSelectionOnPathChange(); + }) + .catch(Notify.handleError); + } // Tree Utility methods $ctrl.toggleUnit = function toggleUnit(unit) { @@ -154,4 +156,7 @@ function NavigationController($location, $rootScope, Tree, AppCache, Notify) { */ $rootScope.$on('$translateChangeSuccess', $ctrl.refreshTranslation); $rootScope.$on('$stateChangeSuccess', updateSelectionOnPathChange); + + // if the session is reloaded, download the new tree units + $rootScope.$on('session:reload', loadTreeUnits); } diff --git a/client/src/js/services/Session.js b/client/src/js/services/Session.js index 17aebaafa2..397b00a36f 100644 --- a/client/src/js/services/Session.js +++ b/client/src/js/services/Session.js @@ -38,6 +38,9 @@ function SessionService($sessionStorage, $http, $location, util, $rootScope) { // logout http method service.logout = logout; + // reloads a user's session + service.reload = reload; + // set the user, enterprise, and project for the session // this should happen right after login function create(user, enterprise, project, paths) { @@ -60,6 +63,7 @@ function SessionService($sessionStorage, $http, $location, util, $rootScope) { // update bindings load(); + // TODO - use $state $location.url('/login'); } @@ -72,7 +76,7 @@ function SessionService($sessionStorage, $http, $location, util, $rootScope) { */ function login(credentials) { /** @todo - should the login reject if a user is already logged in? */ - return $http.post('/login', credentials) + return $http.post('/auth/login', credentials) .then(util.unwrapHttpResponse) .then(function (session) { @@ -95,7 +99,7 @@ function SessionService($sessionStorage, $http, $location, util, $rootScope) { * @return {Promise} promise - the HTTP logout promise */ function logout() { - return $http.get('/logout') + return $http.get('/auth/logout') .then(function () { // destroy the user's session from $storage @@ -115,16 +119,21 @@ function SessionService($sessionStorage, $http, $location, util, $rootScope) { service.enterprise = $storage.enterprise; service.project = $storage.project; service.paths = $storage.paths; - - if($storage.user){ - return $http.post('/reload', { username: $storage.user.username}) - .then(util.unwrapHttpResponse) - .then(function (session) { - service.project = session.project; - service.paths = session.paths; - }); + } + + function reload() { + if ($storage.user) { + return $http.post('/auth/reload', { username: $storage.user.username}) + .then(util.unwrapHttpResponse) + .then(function (session) { + + // re-create the user session in the $storage + create(session.user, session.enterprise, session.project, session.paths); + + // tell the tree to re-download a user's units + $rootScope.$emit('session:reload'); + }); } - } // if the $rootScope emits 'session.destroy', destroy the session diff --git a/client/src/js/services/UserService.js b/client/src/js/services/UserService.js index 0b9f5b307b..8eacdddd71 100644 --- a/client/src/js/services/UserService.js +++ b/client/src/js/services/UserService.js @@ -37,7 +37,7 @@ function UserService($http, util) { var url = (id) ? '/users/' + id : '/users'; return $http.get(url) - .then(util.unwrapHttpResponse); + .then(util.unwrapHttpResponse); } // updates a user with id diff --git a/client/src/js/services/tree.js b/client/src/js/services/tree.js index 28dba6d14e..4b42c96883 100644 --- a/client/src/js/services/tree.js +++ b/client/src/js/services/tree.js @@ -23,7 +23,7 @@ function Tree($http, $translate, util) { .then(util.unwrapHttpResponse); } - /** recusively sort an array of BHIMA units respecting translation keys. */ + /** recursively sort an array of BHIMA units respecting translation keys. */ function sortByTranslationKey(units) { if (angular.isUndefined(units)) { return; diff --git a/server/config/express.js b/server/config/express.js index e0a8ca9008..4666b791fd 100644 --- a/server/config/express.js +++ b/server/config/express.js @@ -88,7 +88,7 @@ exports.configure = function configure(app) { // Only allow routes to use /login, /projects, /logout, and /languages if a // user session does not exists - let publicRoutes = ['/login', '/languages', '/projects/', '/logout']; + let publicRoutes = ['/auth/login', '/languages', '/projects/', '/auth/logout']; app.use(function (req, res, next) { if (_.isUndefined(req.session.user) && !within(req.path, publicRoutes)) { diff --git a/server/config/routes.js b/server/config/routes.js index d0226789d3..90ae01dbbd 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -97,16 +97,16 @@ exports.configure = function configure(app) { app.get('/units', units.list); // auth gateway - app.post('/login', auth.login); - app.get('/logout', auth.logout); - app.post('/reload', auth.reload); + app.post('/auth/login', auth.login); + app.get('/auth/logout', auth.logout); + app.post('/auth/reload', auth.reload); // system and event helpers app.get('/system/events', system.events); app.get('/system/stream', system.stream); app.get('/system/information', system.info); - // dashbord stats + // dashbord stats app.get('/patients/stats', stats.patients); app.get('/invoices/stats', stats.invoices); diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 4245079382..151ec9628f 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -22,13 +22,13 @@ const Forbidden = require('../lib/errors/Forbidden'); const InternalServerError = require('../lib/errors/InternalServerError'); const Topic = require('../lib/topic'); -// POST /login +// POST /auth/login exports.login = login; -// GET /logout +// GET /auth/logout exports.logout = logout; -// POST /reload +// POST /auth/reload exports.reload = reload; /** @@ -45,8 +45,6 @@ function login(req, res, next) { let password = req.body.password; let projectId = req.body.project; - const session = {}; - let sql = ` SELECT user.id, user.username, user.display_name, user.email, project.enterprise_id , project.id AS project_id FROM user JOIN project_permission JOIN project ON @@ -55,253 +53,209 @@ function login(req, res, next) { `; db.exec(sql, [username, password, projectId]) - .then(function (rows) { - - // if no data found, we return a login error - if (rows.length === 0) { - throw new Unauthorized('Bad username and password combination.'); - } - - // we assume only one match for the user - session.user = rows[0]; - - // next make sure this user has permissions - sql = ` - SELECT IF(permission.user_id = ?, 1, 0) authorized, unit.path - FROM unit LEFT JOIN permission - ON unit.id = permission.unit_id; - `; - - return db.exec(sql, [session.user.id]); - }) - .then(modules => { - - let unauthorized = modules.every(mod => !mod.authorized); - - // if no permissions, notify the user that way - if (unauthorized) { - throw new Unauthorized('This user does not have any permissions.'); - } - - session.paths = modules; - - // update the database for when the user logged in - sql = - 'UPDATE user SET user.active = 1, user.last_login = ? WHERE user.id = ?;'; - - return db.exec(sql, [new Date(), session.user.id]); - }) - .then(() => { - - // we need to construct the session on the client side, including: - // the current enterprise - // the current project - sql = ` - SELECT e.id, e.name, e.abbr, e.phone, e.email, BUID(e.location_id) as location_id, e.currency_id, - c.symbol AS currencySymbol, e.po_box, - CONCAT(village.name, ' / ', sector.name, ' / ', province.name) AS location - FROM enterprise AS e - JOIN currency AS c ON e.currency_id = c.id - JOIN village ON village.uuid = e.location_id - JOIN sector ON sector.uuid = village.sector_uuid - JOIN province ON province.uuid = sector.province_uuid - WHERE e.id = ?; - `; - - return db.exec(sql, [session.user.enterprise_id]); - }) - .then(rows => { - - if (!rows.length) { - throw new InternalServerError('There are no enterprises registered in the database!'); - } - - session.enterprise = rows[0]; - - sql = ` - SELECT p.id, p.name, p.abbr, p.enterprise_id - FROM project AS p WHERE p.id = ?; - `; - - return db.exec(sql, [session.user.project_id]); - }) - .then(rows => { - if (!rows.length) { - throw new Unauthorized('No project matching the provided id.'); - } - - session.project = rows[0]; - - // bind the session variables - req.session.project = session.project; - req.session.user = session.user; - req.session.enterprise = session.enterprise; - req.session.paths = session.paths; - - // broadcast LOGIN event - Topic.publish(Topic.channels.APP, { - event: Topic.events.LOGIN, - entity: Topic.entities.USER, - user_id : req.session.user.id, - id: session.user.id - }); - - // send the session data back to the client - res.status(200).json(session); - }) - .catch(next) - .done(); + .then(function (rows) { + + // if no data found, we return a login error + if (rows.length === 0) { + throw new Unauthorized('Bad username and password combination.'); + } + + return loadSessionInformation(rows[0]); + }) + .then(session => { + + // bind the session variables + req.session.project = session.project; + req.session.user = session.user; + req.session.enterprise = session.enterprise; + req.session.paths = session.paths; + + // broadcast LOGIN event + Topic.publish(Topic.channels.APP, { + event: Topic.events.LOGIN, + entity: Topic.entities.USER, + user_id : req.session.user.id, + id: session.user.id + }); + + // send the session data back to the client + res.status(200).json(session); + }) + .catch(next) + .done(); + } + + /** + * @method logout + * + * Destroys the server side session and sets the user as inactive. + */ + function logout(req, res, next) { + let sql = + 'UPDATE user SET user.active = 0 WHERE user.id = ?;'; + + db.exec(sql, [req.session.user.id]) + .then(() => { + + // broadcast LOGOUT event + Topic.publish(Topic.channels.APP, { + event: Topic.events.LOGOUT, + entity: Topic.entities.USER, + user_id : req.session.user.id, + id: req.session.user.id, + }); + + // destroy the session + req.session.destroy(); + res.sendStatus(200); + }) + .catch(next) + .done(); } /** - * @method logout - * - * Destroys the server side session and sets the user as inactive. - */ -function logout(req, res, next) { - let sql = - 'UPDATE user SET user.active = 0 WHERE user.id = ?;'; - - db.exec(sql, [req.session.user.id]) - .then(() => { - - // broadcast LOGOUT event - Topic.publish(Topic.channels.APP, { - event: Topic.events.LOGOUT, - entity: Topic.entities.USER, - user_id : req.session.user.id, - id: req.session.user.id, - }); - - // destroy the session - req.session.destroy(); - res.sendStatus(200); - }) - .catch(next) - .done(); -} - - -// POST /reload -exports.reload = reload; - -/** - * @method reload + * @function loadSessionInformation * * @description - * Logs a client into the server. The /login route accepts a POST request with - * a username, and project id. It checks if the username - * exist in the database, then verifies that the user has permission to access - * the database all enterprise, project, and user data for easy access. + * This method takes in a user object (with an id) and loads all the session + * information about it. This can be used to populate or refresh req.session + * in case there are user changes that are made (such as to the enterprise, + * project, or otherwise). + * + * @param {Object} user - the user object to look up the session + * + * @returns {Promise} - a promise resolving to the session + * + * @private */ -function reload(req, res, next) { - let username = req.body.username; - let projectId = req.body.project; +function loadSessionInformation(user) { + // this will be the new session const session = {}; let sql = ` SELECT user.id, user.username, user.display_name, user.email, project.enterprise_id , project.id AS project_id FROM user JOIN project_permission JOIN project ON user.id = project_permission.user_id AND project.id = project_permission.project_id - WHERE user.username = ?; + WHERE user.id = ?; `; - db.exec(sql, [username]) - .then(function (rows) { - - // if no data found, we return a login error - if (rows.length === 0) { - throw new Unauthorized('Bad username and project combination.'); - } - - // we assume only one match for the user - session.user = rows[0]; - - // next make sure this user has permissions - sql = ` - SELECT IF(permission.user_id = ?, 1, 0) authorized, unit.path - FROM unit LEFT JOIN permission - ON unit.id = permission.unit_id; - `; - - return db.exec(sql, [session.user.id]); - }) - .then(modules => { - - let unauthorized = modules.every(mod => !mod.authorized); - - // if no permissions, notify the user that way - if (unauthorized) { - throw new Unauthorized('This user does not have any permissions.'); - } - - session.paths = modules; - - // update the database for when the user logged in - sql = - 'UPDATE user SET user.active = 1, user.last_login = ? WHERE user.id = ?;'; - - return db.exec(sql, [new Date(), session.user.id]); - }) - .then(() => { - - // we need to construct the session on the client side, including: - // the current enterprise - // the current project - sql = ` - SELECT e.id, e.name, e.abbr, e.phone, e.email, BUID(e.location_id) as location_id, e.currency_id, - c.symbol AS currencySymbol, e.po_box, - CONCAT(village.name, ' / ', sector.name, ' / ', province.name) AS location - FROM enterprise AS e - JOIN currency AS c ON e.currency_id = c.id - JOIN village ON village.uuid = e.location_id - JOIN sector ON sector.uuid = village.sector_uuid - JOIN province ON province.uuid = sector.province_uuid - WHERE e.id = ?; - `; - - return db.exec(sql, [session.user.enterprise_id]); - }) - .then(rows => { - - if (!rows.length) { - throw new InternalServerError('There are no enterprises registered in the database!'); - } - - session.enterprise = rows[0]; - - sql = ` - SELECT p.id, p.name, p.abbr, p.enterprise_id - FROM project AS p WHERE p.id = ?; - `; - - return db.exec(sql, [session.user.project_id]); - }) - .then(rows => { - if (!rows.length) { - throw new Unauthorized('No project matching the provided id.'); - } - - session.project = rows[0]; - - // bind the session variables - req.session.project = session.project; - req.session.user = session.user; - req.session.enterprise = session.enterprise; - req.session.paths = session.paths; - - // broadcast LOGIN event - Topic.publish(Topic.channels.APP, { - event: Topic.events.LOGIN, - entity: Topic.entities.USER, - user_id : req.session.user.id, - id: session.user.id + return db.exec(sql, [user.id]) + .then(rows => { + + // if no data found, we return a login error + if (rows.length === 0) { + throw new InternalServerError(`Server could not locate user with id ${user.id}`); + } + + // we assume only one match for the user + session.user = rows[0]; + + // next make sure this user has permissions + sql = ` + SELECT IF(permission.user_id = ?, 1, 0) authorized, unit.path + FROM unit LEFT JOIN permission + ON unit.id = permission.unit_id; + `; + + return db.exec(sql, [session.user.id]); + }) + .then(modules => { + + let unauthorized = modules.every(mod => !mod.authorized); + + // if no permissions, notify the user that way + if (unauthorized) { + throw new Unauthorized('This user does not have any permissions.'); + } + + session.paths = modules; + + // update the database for when the user logged in + sql = + 'UPDATE user SET user.active = 1, user.last_login = ? WHERE user.id = ?;'; + + return db.exec(sql, [new Date(), session.user.id]); + }) + .then(() => { + + // we need to construct the session on the client side, including: + // the current enterprise + // the current project + sql = ` + SELECT e.id, e.name, e.abbr, e.phone, e.email, BUID(e.location_id) as location_id, e.currency_id, + c.symbol AS currencySymbol, e.po_box, + CONCAT(village.name, ' / ', sector.name, ' / ', province.name) AS location + FROM enterprise AS e + JOIN currency AS c ON e.currency_id = c.id + JOIN village ON village.uuid = e.location_id + JOIN sector ON sector.uuid = village.sector_uuid + JOIN province ON province.uuid = sector.province_uuid + WHERE e.id = ?; + `; + + return db.exec(sql, [session.user.enterprise_id]); + }) + .then(rows => { + + if (!rows.length) { + throw new InternalServerError('There are no enterprises registered in the database!'); + } + + session.enterprise = rows[0]; + + sql = ` + SELECT p.id, p.name, p.abbr, p.enterprise_id + FROM project AS p WHERE p.id = ?; + `; + + return db.exec(sql, [session.user.project_id]); + }) + .then(rows => { + if (!rows.length) { + throw new Unauthorized('No project matching the provided id.'); + } + + session.project = rows[0]; + + return session; }); +} + - // send the session data back to the client - res.status(200).json(session); - }) - .catch(next) - .done(); -} \ No newline at end of file +/** + * @method reload + * + * @description + * Uses the same login code to re + */ +function reload(req, res, next) { + + if (!(req.session && req.session.user)) { + return next(new Unauthorized('The user is not signed in.')); + } + + // refresh the user's session by manually calling refresh session + loadSessionInformation(req.session.user) + .then(session => { + + // bind the session variables + req.session.project = session.project; + req.session.user = session.user; + req.session.enterprise = session.enterprise; + req.session.paths = session.paths; + + // broadcast LOGIN event + Topic.publish(Topic.channels.APP, { + event: Topic.events.RELOAD, + entity: Topic.entities.USER, + user_id : req.session.user.id, + id: session.user.id + }); + + // send the session data back to the client + res.status(200).json(session); + }) + .catch(next) + .done(); +} diff --git a/server/lib/topic.js b/server/lib/topic.js index 37cbd2f418..ee5ed65621 100644 --- a/server/lib/topic.js +++ b/server/lib/topic.js @@ -29,6 +29,7 @@ const events = { DELETE: 'delete', REPORT: 'report', LOGIN: 'login', + RELOAD : 'reload', LOGOUT: 'logout', SEARCH: 'search' }; diff --git a/test/client-unit/services/SessionService.spec.js b/test/client-unit/services/SessionService.spec.js index b527672653..7307ee972b 100644 --- a/test/client-unit/services/SessionService.spec.js +++ b/test/client-unit/services/SessionService.spec.js @@ -37,11 +37,14 @@ describe('SessionService', function () { httpBackend = $httpBackend; // mocked responses - httpBackend.when('POST', '/login') + httpBackend.when('POST', '/auth/login') .respond(200, { user : user, project : project, enterprise : enterprise }); - httpBackend.when('GET', '/logout') + httpBackend.when('GET', '/auth/logout') .respond(200); + + httpBackend.when('POST', '/auth/reload') + .respond(200, { user : user, project : project, enterprise : enterprise }); })); // make sure $http is clean after tests @@ -77,7 +80,7 @@ describe('SessionService', function () { Session.login(user); // expect the HTTP backend to have been hit - httpBackend.expectPOST('/login'); + httpBackend.expectPOST('/auth/login'); httpBackend.flush(); // the event should have been emitted @@ -91,7 +94,7 @@ describe('SessionService', function () { Session.logout(); // expect the HTTP backend to have been hit - httpBackend.expectGET('/logout'); + httpBackend.expectGET('/auth/logout'); httpBackend.flush(); // the event should have been emitted diff --git a/test/integration/login.js b/test/integration/login.js index 294348b427..2ff3c77c69 100644 --- a/test/integration/login.js +++ b/test/integration/login.js @@ -3,7 +3,7 @@ const helpers = require('./helpers'); -describe('(/login) The login API', function () { +describe('(/auth/login) The login API', function () { 'use strict'; const port = process.env.PORT || 8080; @@ -52,7 +52,7 @@ describe('(/login) The login API', function () { it('rejects an unrecognized user', function () { return chai.request(url) - .post('/login') + .post('/auth/login') .send(invalidUser) .then(function (res) { helpers.api.errored(res, 401); @@ -64,7 +64,7 @@ describe('(/login) The login API', function () { it('rejects a recognized user user without a project', function () { return chai.request(url) - .post('/login') + .post('/auth/login') .send({ username : validUser.username, password : validUser.password }) .then(function (res) { helpers.api.errored(res, 401); @@ -75,7 +75,7 @@ describe('(/login) The login API', function () { it('rejects a recognized user without a password', function () { return chai.request(url) - .post('/login') + .post('/auth/login') .send({ username : validUser.username, project : validUser.project }) .then(function (res) { helpers.api.errored(res, 401); @@ -86,7 +86,7 @@ describe('(/login) The login API', function () { it('sets a user\'s session properties', function () { return chai.request(url) - .post('/login') + .post('/auth/login') .send(validUser) .then(function (res) { expect(res).to.have.status(200); diff --git a/test/integration/setup.js b/test/integration/setup.js index b015f0b2b7..24353f4708 100644 --- a/test/integration/setup.js +++ b/test/integration/setup.js @@ -46,7 +46,7 @@ before(() => { const user = { username : 'superuser', password : 'superuser', project: 1 }; // trigger login - return (() => agent.post('/login').send(user))(); + return (() => agent.post('/auth/login').send(user))(); }); // runs after all tests are completed