diff --git a/client/configuration/config.js b/client/configuration/config.js index b808e1d..679e42a 100644 --- a/client/configuration/config.js +++ b/client/configuration/config.js @@ -1,5 +1,34 @@ angular.module('config', []) -.constant('ENV', {RequiresAuth:false,etcdHost:'127.0.0.1',etcdPort:'4001',hobknobHost:'localhost',hobknobPort:'3006',categories:[{id:0,name:'Simple Features',description:'Use when you want your feature to be either on or off'},{id:1,name:'Domain Features',description:'Use when you want your features to be toggled separately for different domains (e.g. com, couk, fr, ...)',values:['com','couk','de','fr']},{id:2,name:'Locale Features',description:'Use when you want your features to be toggled separately for different locales (e.g. en-GB, en-US, fr-FR, ...)',values:['en-GB','en-US','fr-FR','de-DE']}],plugin:{path:'../server/examplePlugin.js'},hooks:['server/exampleHook.js']}) - -; \ No newline at end of file +.constant('ENV', { + RequiresAuth: false, + dataSource: 'etcd', + etcdHost: '127.0.0.1', + etcdPort: '4001', + hobknobHost: 'localhost', + hobknobPort: '3006', + loadBalancerFile: '/etc/lbstatus/hobknob', + categories: [{ + id: 0, + name: 'Simple Features', + description: 'Use when you want your feature to be either on or off' + }, { + id: 1, + name: 'Domain Features', + description: 'Use when you want your features to be toggled separately for different domains (e.g. com, couk, fr, ...)', + values: ['com', 'couk', 'de', 'fr'] + }, { + id: 2, + name: 'Locale Features', + description: 'Use when you want your features to be toggled separately for different locales (e.g. en-GB, en-US, fr-FR, ...)', + values: ['en-GB', 'en-US', 'fr-FR', 'de-DE'] + }], + plugin: { + path: '../server/examplePlugin.js' + }, + hooks: ['server/exampleHook.js'], + customization: { + logo: '/img/opentable-hr-mono.png', + title: 'Opentable Feature Toggles' + } +}); diff --git a/client/js/app.js b/client/js/app.js index 26678d8..82a2396 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -26,6 +26,9 @@ featureToggleFrontend.config(function ($routeProvider, $locationProvider, $httpP $httpProvider.defaults.headers.post = {'Content-Type': 'application/json'}; }); -featureToggleFrontend.run(function (editableOptions) { +featureToggleFrontend.run(function ($rootScope, editableOptions, ENV) { editableOptions.theme = 'bs3'; + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + $rootScope.title = current.$$route.title || ENV.customization.title; + }); }); diff --git a/client/js/controllers/loginController.js b/client/js/controllers/loginController.js index f60fb64..3002a94 100644 --- a/client/js/controllers/loginController.js +++ b/client/js/controllers/loginController.js @@ -1,6 +1,8 @@ 'use strict'; featureToggleFrontend.controller('LoginController', ['$scope', '$window', 'ENV', function ($scope, $window, ENV) { + $scope.logo = ENV.customization.logo; + if (ENV.RequiresAuth) { var authProvider = Object.keys(ENV.AuthProviders)[0]; diff --git a/client/js/controllers/sideBarController.js b/client/js/controllers/sideBarController.js index d8fa8ea..8d53c6f 100644 --- a/client/js/controllers/sideBarController.js +++ b/client/js/controllers/sideBarController.js @@ -1,10 +1,11 @@ 'use strict'; -featureToggleFrontend.controller('SideBarController', ['$scope', 'applicationService', 'authorisationService', '$location', 'focus', 'CurrentUser', function ($scope, applicationService, authorisationService, $location, focus, CurrentUser) { +featureToggleFrontend.controller('SideBarController', ['$scope', 'applicationService', 'authorisationService', '$location', 'focus', 'CurrentUser', 'ENV', function ($scope, applicationService, authorisationService, $location, focus, CurrentUser, ENV) { $scope.applications = []; $scope.newApplicationName = ''; $scope.adding = false; $scope.CurrentUser = CurrentUser; + $scope.logo = ENV.customization.logo; var loadApplications = function () { applicationService.getApplications( diff --git a/client/views/login.pug b/client/views/login.pug index 3a5dbe6..39eb8da 100644 --- a/client/views/login.pug +++ b/client/views/login.pug @@ -1,21 +1,21 @@ doctype html -html(lang='en') +html(lang='en' ng-app='featureToggleFrontend' ng-cloak) head meta(name='viewport', content='width=device-width, initial-scale=1') - title Hobknob Dashboard + title(ng-bind="$root.title") link(href='/bower_components/bootstrap/dist/css/bootstrap.min.css', media='all', rel='stylesheet') link(href='/css/dashboard.css', media='all', rel='stylesheet') link(href='/css/toggleswitch.css', media='all', rel='stylesheet') link(href='/bower_components/bootstrap-social/bootstrap-social.css', media='all', rel='stylesheet') link(href='/bower_components/font-awesome/css/font-awesome.css', media='all', rel='stylesheet') script(src='/bower_components/jquery/dist/jquery.min.js') - body(ng-app='featureToggleFrontend' ng-cloak) + body #wrapper #sidebar-wrapper ul.sidebar-nav(ng-controller="LoginController") li.sidebar-brand a.navbar-brand.logo(href='/') - img(src='/img/opentable-hr-mono.png') + img(ng-src='{{logo}}') a(href='/auth/google',ng-if="authProvider == 'GoogleAuth'", id="loginwithgoogle").btn.btn-block.btn-social.btn-google-plus.sign-in i.fa.fa-google-plus | Sign in with Google diff --git a/client/views/main.pug b/client/views/main.pug index cf85370..8b08094 100644 --- a/client/views/main.pug +++ b/client/views/main.pug @@ -1,8 +1,8 @@ doctype html -html(lang='en') +html(lang='en' ng-app='featureToggleFrontend') head meta(name='viewport', content='width=device-width, initial-scale=1') - title Hobknob Dashboard + title(ng-bind="$root.title") link(href='/bower_components/bootstrap/dist/css/bootstrap.min.css', media='all', rel='stylesheet') link(href='/css/dashboard.css', media='all', rel='stylesheet') link(href='/bower_components/angular-xeditable/dist/css/xeditable.css' rel='stylesheet') @@ -11,7 +11,7 @@ html(lang='en') script(src='/bower_components/jquery/dist/jquery.min.js') script(type="text/javascript"). window.user = !{JSON.stringify(user || {}, null, 2)}; - body(ng-app='featureToggleFrontend') + body #wrapper include ./sidebar.pug #page-content-wrapper diff --git a/client/views/sidebar.pug b/client/views/sidebar.pug index 0a1ad6a..837d11e 100644 --- a/client/views/sidebar.pug +++ b/client/views/sidebar.pug @@ -2,7 +2,7 @@ ul.sidebar-nav li.sidebar-brand a.navbar-brand.logo(href='/') - img(src='/img/opentable-hr-mono.png') + img(ng-src='{{logo}}') li .profile img.media-object.img-circle(ng-src='{{CurrentUser.getPicture()}}', alt='avatar') diff --git a/config/default.json b/config/default.json index 6f545ab..27fc7e7 100644 --- a/config/default.json +++ b/config/default.json @@ -2,7 +2,7 @@ "RequiresAuth": false, "dataSource": "etcd", "etcdHost": "127.0.0.1", - "etcdPort": "4001", + "etcdPort": "2379", "hobknobHost": "localhost", "hobknobPort": "3006", "loadBalancerFile": "/etc/lbstatus/hobknob", @@ -40,5 +40,9 @@ }, "hooks": [ "server/exampleHook.js" - ] + ], + "customization": { + "logo": "/img/opentable-hr-mono.png", + "title": "Hobknob Dashboard" + } } diff --git a/server/app.js b/server/app.js index a16cd37..a1d1778 100644 --- a/server/app.js +++ b/server/app.js @@ -95,6 +95,8 @@ var passportGoogleAuthenticateParams = function () { app.use(express.static(path.join(__dirname, '/../client'))); + +// TODO: all this can be moved out into routes/handlers app.get('/login', authenticateRoutes.login); app.get('/', ensureAuthenticatedOrRedirectToLogin, dashboardRoutes.dashboard); app.get('/partials/:name', dashboardRoutes.partials); diff --git a/server/domain/application.js b/server/domain/application.js index 1bc75c8..de2a4b6 100644 --- a/server/domain/application.js +++ b/server/domain/application.js @@ -1,6 +1,10 @@ 'use strict'; var config = require('config'); +var common = require('./common'); +var audit = require('./audit'); +var acl = require('./acl'); + var application = function() { switch (config.dataSource.toLowerCase()) { case 'etcd': @@ -17,11 +21,58 @@ module.exports = { }, addApplication: function (applicationName, req, cb) { - application().addApplication(applicationName, req, cb); + application().addApplication(applicationName, req, function(err) { + if (err) { + return cb(err); + } + + var userDetails = common.getUserDetails(req) + + audit.addApplicationAudit(userDetails, applicationName, 'Created', function () { + if (err) { + cb(err); + return; + } + }); + + if (config.RequiresAuth) { + var userEmail = userDetails.email.toLowerCase(); // todo: need better user management + acl.grant(userEmail, applicationName, function (grantErr) { + if (grantErr) { + cb(grantErr); + return; + } + cb(); + }); + } else { + cb(); + } + }); }, deleteApplication: function (applicationName, req, cb) { - application().deleteApplication(applicationName, req, cb); + application().deleteApplication(applicationName, req, function (err) { + if (err) { + return cb(err); + } + + audit.addApplicationAudit(common.getUserDetails(req), applicationName, 'Deleted', function () { + if (err) { + console.log(err); + } + }); + + if (config.RequiresAuth) { + acl.revokeAll(applicationName, function (revokeErr) { + if (revokeErr) { + return cb(revokeErr); + } + cb(); + }); + } else { + cb(); + } + }); }, getApplicationMetaData: function (applicationName, cb) { diff --git a/server/domain/common.js b/server/domain/common.js new file mode 100644 index 0000000..f8cda2f --- /dev/null +++ b/server/domain/common.js @@ -0,0 +1,9 @@ +'use strict'; + +var config = require('config'); + +module.exports = { + getUserDetails: function (req) { + return config.RequiresAuth ? req.user._json : {name: 'Anonymous'}; + } +}; diff --git a/server/domain/etcd/application.js b/server/domain/etcd/application.js index 606c7b0..074827f 100644 --- a/server/domain/etcd/application.js +++ b/server/domain/etcd/application.js @@ -7,10 +7,6 @@ var acl = require('../acl'); var audit = require('../audit'); var etcdBaseUrl = 'http://' + config.etcdHost + ':' + config.etcdPort + '/v2/keys/'; -var getUserDetails = function (req) { - return config.RequiresAuth ? req.user._json : {name: 'Anonymous'}; -}; - module.exports = { getApplications: function (cb) { etcd.client.get('v1/toggles/', {recursive: false}, function (err, result) { @@ -31,58 +27,13 @@ module.exports = { }, addApplication: function (applicationName, req, cb) { - var path = 'v1/toggles/' + applicationName; - etcd.client.mkdir(path, function (err) { - if (err) { - return cb(err); - } - - audit.addApplicationAudit(getUserDetails(req), applicationName, 'Created', function () { - if (err) { - console.log(err); // todo: better logging - } - }); - - // todo: not sure if this is correct - if (config.RequiresAuth) { - var userEmail = getUserDetails(req).email.toLowerCase(); // todo: need better user management - acl.grant(userEmail, applicationName, function (grantErr) { - if (grantErr) { - cb(grantErr); - return; - } - cb(); - }); - } else { - cb(); - } - }); + var path = 'v1/toggles/' + applicationName; + etcd.client.mkdir(path, cb); }, deleteApplication: function (applicationName, req, cb) { var path = 'v1/toggles/' + applicationName; - etcd.client.delete(path, {recursive: true}, function (err) { - if (err) { - return cb(err); - } - - audit.addApplicationAudit(getUserDetails(req), applicationName, 'Deleted', function () { - if (err) { - console.log(err); - } - }); - - if (config.RequiresAuth) { - acl.revokeAll(applicationName, function (revokeErr) { - if (revokeErr) { - return cb(revokeErr); - } - cb(); - }); - } else { - cb(); - } - }); + etcd.client.delete(path, {recursive: true}, cb); }, getApplicationMetaData: function (applicationName, cb) { diff --git a/server/domain/etcd/audit.js b/server/domain/etcd/audit.js index ab60054..45c8ef1 100644 --- a/server/domain/etcd/audit.js +++ b/server/domain/etcd/audit.js @@ -9,6 +9,11 @@ module.exports = { var path = 'v1/audit/application/' + applicationName; etcd.client.get(path, {recursive: true}, function (err, result) { if (err) { + if (err.errorCode === 100) { // key not found + callback(null, []); + return; + } + callback(err); return; } @@ -26,6 +31,11 @@ module.exports = { var path = 'v1/audit/feature/' + applicationName + '/' + featureName; etcd.client.get(path, {recursive: true}, function (err, result) { if (err) { + if (err.errorCode === 100) { // key not found + callback(null, []); + return; + } + callback(err); return; } diff --git a/server/domain/etcd/feature.js b/server/domain/etcd/feature.js index 297b25c..2fdebd9 100644 --- a/server/domain/etcd/feature.js +++ b/server/domain/etcd/feature.js @@ -3,6 +3,7 @@ var etcd = require('./etcd'); var _ = require('underscore'); var config = require('config'); +var common = require('../common'); var acl = require('../acl'); var category = require('../category'); var etcdBaseUrl = 'http://' + config.etcdHost + ':' + config.etcdPort + '/v2/keys/'; @@ -13,10 +14,6 @@ var isMetaNode = function (node) { return s(node.key).endsWith('@meta'); }; -var getUserDetails = function (req) { - return config.RequiresAuth ? req.user._json : {name: 'Anonymous'}; -}; - var getMetaData = function (featureNode) { var metaNode = _.find(featureNode.nodes, function (n) { return isMetaNode(n); @@ -45,7 +42,7 @@ var getSimpleFeature = function (name, node, description) { description: description, values: [value], categoryId: 0, - fullPath: etcdBaseUrl + 'v1/toggles/' + name + fullPath: etcdBaseUrl + 'v1/toggles/' + name // TODO: determine why this is needed }; }; @@ -62,7 +59,7 @@ var getMultiFeature = function (name, node, metaData, categories, description) { description: description, values: values, categoryId: metaData.categoryId, - fullPath: etcdBaseUrl + 'v1/toggles/' + name + fullPath: etcdBaseUrl + 'v1/toggles/' + name // TODO: determine why this is needed }; }; @@ -268,7 +265,7 @@ var addMultiFeature = function (path, applicationName, featureName, featureDescr hooks.run({ fn: 'addFeature', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName, value: false @@ -288,7 +285,7 @@ var addSimpleFeature = function (path, applicationName, featureName, featureDesc hooks.run({ fn: 'addFeatureToggle', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName, toggleName: null, @@ -325,7 +322,7 @@ module.exports.updateFeatureToggle = function (applicationName, featureName, val hooks.run({ fn: 'updateFeatureToggle', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName, toggleName: null, @@ -350,7 +347,7 @@ module.exports.addFeatureToggle = function (applicationName, featureName, toggle hooks.run({ fn: 'addFeatureToggle', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName, toggleName: toggleName, @@ -371,7 +368,7 @@ module.exports.updateFeatureMultiToggle = function (applicationName, featureName hooks.run({ fn: 'updateFeatureToggle', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName, toggleName: toggleName, @@ -389,7 +386,7 @@ module.exports.deleteFeature = function (applicationName, featureName, req, cb) hooks.run({ fn: 'deleteFeature', - user: getUserDetails(req), + user: common.getUserDetails(req), applicationName: applicationName, featureName: featureName }); diff --git a/server/routes/auditRoutes.js b/server/routes/auditRoutes.js index 9097178..e2def39 100644 --- a/server/routes/auditRoutes.js +++ b/server/routes/auditRoutes.js @@ -9,10 +9,6 @@ module.exports = { audit.getFeatureAuditTrail(applicationName, featureName, function (err, auditTrail) { if (err) { - if (err.errorCode === 100) { // key not found - res.send([]); - return; - } throw err; } res.send(auditTrail); @@ -24,10 +20,6 @@ module.exports = { audit.getApplicationAuditTrail(applicationName, function (err, auditTrail) { if (err) { - if (err.errorCode === 100) { // key not found - res.send([]); - return; - } throw err; } res.send(auditTrail);