From c2c93d4e323d7b113fa5e70a2cc4d6a28a44b965 Mon Sep 17 00:00:00 2001 From: Vincent Fiduccia Date: Fri, 19 Dec 2014 17:38:58 -0700 Subject: [PATCH] Support for GitHub authentication --- .ember-cli | 2 +- Brocfile.js | 2 + app/initializers/authentication.js | 49 +++ app/initializers/session.js | 14 + app/initializers/store.js | 28 +- app/mixins/authenticated-route.js | 13 + app/pods/application/controller.js | 18 +- app/pods/application/route.js | 255 +--------------- app/pods/application/template.hbs | 10 +- app/pods/application/view.js | 2 - app/pods/authenticated/controller.js | 16 + app/pods/authenticated/index/route.js | 7 + app/pods/authenticated/route.js | 282 ++++++++++++++++++ app/pods/authenticated/template.hbs | 5 + app/pods/authenticated/view.js | 5 + .../components/github-avatar/component.js | 29 ++ .../components/github-avatar/template.hbs | 16 + app/pods/components/page-header/template.hbs | 40 ++- app/pods/hosts/index/template.hbs | 2 + app/pods/hosts/route.js | 5 - app/pods/login/controller.js | 83 ++++++ app/pods/login/route.js | 10 + app/pods/login/template.hbs | 25 ++ app/pods/login/view.js | 11 + app/pods/logout/route.js | 9 + app/pods/settings/auth/controller.js | 176 +++++++++++ app/pods/settings/auth/route.js | 15 + app/pods/settings/auth/template.hbs | 170 +++++++++++ app/pods/settings/auth/view.js | 7 + app/pods/settings/route.js | 4 + app/pods/settings/template.hbs | 1 + app/pods/volumes/route.js | 5 - app/router.js | 50 ++-- app/routes/fail-whale.js | 10 +- app/routes/index.js | 2 +- app/routes/loading.js | 2 +- app/styles/app.scss | 1 + app/styles/github-avatar.scss | 43 +++ app/styles/layout.scss | 33 +- app/templates/fail-whale.hbs | 22 +- app/utils/local-storage.js | 35 +++ app/utils/session-storage.js | 35 +++ .../route/files/app/__path__/__name__.js | 2 +- bower.json | 3 +- config/environment.js | 9 +- package.json | 8 +- server/proxies/api.js | 2 +- tests/unit/pods/authenticated/route-test.js | 14 + tests/unit/pods/login/route-test.js | 14 + tests/unit/pods/logout/route-test.js | 14 + tests/unit/pods/settings/auth/route-test.js | 14 + tests/unit/pods/settings/route-test.js | 14 + 52 files changed, 1304 insertions(+), 339 deletions(-) create mode 100644 app/initializers/authentication.js create mode 100644 app/initializers/session.js create mode 100644 app/mixins/authenticated-route.js create mode 100644 app/pods/authenticated/controller.js create mode 100644 app/pods/authenticated/index/route.js create mode 100644 app/pods/authenticated/route.js create mode 100644 app/pods/authenticated/template.hbs create mode 100644 app/pods/authenticated/view.js create mode 100644 app/pods/components/github-avatar/component.js create mode 100644 app/pods/components/github-avatar/template.hbs create mode 100644 app/pods/login/controller.js create mode 100644 app/pods/login/route.js create mode 100644 app/pods/login/template.hbs create mode 100644 app/pods/login/view.js create mode 100644 app/pods/logout/route.js create mode 100644 app/pods/settings/auth/controller.js create mode 100644 app/pods/settings/auth/route.js create mode 100644 app/pods/settings/auth/template.hbs create mode 100644 app/pods/settings/auth/view.js create mode 100644 app/pods/settings/route.js create mode 100644 app/pods/settings/template.hbs create mode 100644 app/styles/github-avatar.scss create mode 100644 app/utils/local-storage.js create mode 100644 app/utils/session-storage.js create mode 100644 tests/unit/pods/authenticated/route-test.js create mode 100644 tests/unit/pods/login/route-test.js create mode 100644 tests/unit/pods/logout/route-test.js create mode 100644 tests/unit/pods/settings/auth/route-test.js create mode 100644 tests/unit/pods/settings/route-test.js diff --git a/.ember-cli b/.ember-cli index d9391589c..6dabb90f4 100644 --- a/.ember-cli +++ b/.ember-cli @@ -7,5 +7,5 @@ */ "disableAnalytics": true, "port": 8000, - "host": "127.0.0.1" + "host": "0.0.0.0" } diff --git a/Brocfile.js b/Brocfile.js index 2797ccb3d..5ade88658 100644 --- a/Brocfile.js +++ b/Brocfile.js @@ -49,5 +49,7 @@ app.import('bower_components/c3/c3.css'); app.import('vendor/term.js/src/term.js'); app.import('bower_components/bootstrap-multiselect/dist/js/bootstrap-multiselect.js'); app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css'); +app.import('bower_components/bootstrap-switch/dist/js/bootstrap-switch.js'); +app.import('bower_components/bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css'); module.exports = app.toTree(); diff --git a/app/initializers/authentication.js b/app/initializers/authentication.js new file mode 100644 index 000000000..541a53007 --- /dev/null +++ b/app/initializers/authentication.js @@ -0,0 +1,49 @@ +import Ember from 'ember'; +import config from 'torii/configuration'; +import bootstrap from 'torii/bootstrap/torii'; + +export function initialize(container, application) { + application.deferReadiness(); + var store = container.lookup('store:main'); + + // Find out if auth is enabled + store.rawRequest({ + url: 'token', // Base url, which will be /v1 + headers: { 'authorization': undefined } + }) + .then(function(obj) { + var body = JSON.parse(obj.xhr.responseText); + var token = body.data[0]; + + application.set('hasAuthentication', true); + application.set('authenticationEnabled', token.security); + configureTorii(token.clientId); + + return Ember.RSVP.resolve(undefined,'API supports authentication'); + }) + .catch(function(obj) { + application.set('hasAuthentication', false); + application.set('authenticationEnabled', false); + application.set('initError', obj); + return Ember.RSVP.resolve(undefined,'Error determining API authentication'); + }) + .finally(function() { + application.advanceReadiness(); + }); + + function configureTorii(clientId) { + config.providers['github-oauth2'] = { + apiKey: clientId, + scope: 'read:org' + }; + + bootstrap(container); + application.inject('controller', 'torii', 'torii:main'); + } +} + +export default { + name: 'authentication', + after: 'store', + initialize: initialize +}; diff --git a/app/initializers/session.js b/app/initializers/session.js new file mode 100644 index 000000000..dffd9386e --- /dev/null +++ b/app/initializers/session.js @@ -0,0 +1,14 @@ +import SessionStorage from 'ui/utils/session-storage'; + +export function initialize(container, application) { + container.register('session:main', SessionStorage); + application.inject('controller', 'session', 'session:main'); + application.inject('route', 'session', 'session:main'); + application.inject('model', 'session', 'session:main'); +} + +export default { + name: 'session', + before: 'store', + initialize: initialize +}; diff --git a/app/initializers/store.js b/app/initializers/store.js index 7aa95f6e6..77ecff9e0 100644 --- a/app/initializers/store.js +++ b/app/initializers/store.js @@ -1,13 +1,33 @@ import Ember from 'ember'; export function initialize(container, application) { - application.deferReadiness(); - var store = container.lookup('store:main'); + var session = container.lookup('session:main'); store.set('removeAfterDelete', false); store.reopen({ baseUrl: application.apiEndpoint, + + headers: function() { + var out = { + 'x-api-no-challenge': 'true', // Don't send me www-authenticate headers + }; + + var token = session.get('token'); + if ( token ) + { + out['authorization'] = 'Bearer ' + token; + } + + var accountId = session.get('accountId'); + if ( accountId ) + { + out['x-api-project-id'] = accountId; + } + + return out; + }.property().volatile(), + reallyAll: store.all, all: function(type) { type = this.normalizeType(type); @@ -27,10 +47,6 @@ export function initialize(container, application) { return proxy; } }); - - store.find('schema', null, {url: 'schemas'}).then(function() { - application.advanceReadiness(); - }); } export default { diff --git a/app/mixins/authenticated-route.js b/app/mixins/authenticated-route.js new file mode 100644 index 000000000..77c37b2fa --- /dev/null +++ b/app/mixins/authenticated-route.js @@ -0,0 +1,13 @@ +import Ember from "ember"; + +export default Ember.Mixin.create({ + beforeModel: function(transition) { + this._super(transition); + var isLoggedIn = (this.get('session.isLoggedIn') === '1') && this.get('session.token'); + if ( this.get('app.authenticationEnabled') && !isLoggedIn ) + { + transition.send('logout',transition,true); + return Ember.RSVP.reject('Not logged in'); + } + } +}); diff --git a/app/pods/application/controller.js b/app/pods/application/controller.js index 01a1ce14b..3b342f464 100644 --- a/app/pods/application/controller.js +++ b/app/pods/application/controller.js @@ -1,19 +1,8 @@ import Ember from "ember"; export default Ember.Controller.extend({ - navExpand: (parseInt($.cookie('navExpand'),10) !== 0), error: null, - pageName: '', - - navExpandChange: function() { - var inAYear = new Date(); - inAYear.setYear(inAYear.getFullYear()+1); - - $.cookie('navExpand', (this.get('navExpand') ? 1 : 0), { - expires: inAYear - }); - }.observes('navExpand'), - + requiresAuthentication: null, absoluteEndpoint: function() { var url = this.get('app.endpoint'); @@ -23,11 +12,8 @@ export default Ember.Controller.extend({ { url = window.location.origin + '/' + url.replace(/^\/+/,''); } - else if ( url.charAt(url.length-1) !== '/' ) { - url = url + "/"; - } - // Url must end in a single slash + // URL must end in a single slash url = url.replace(/\/+$/,'') + '/'; return url; diff --git a/app/pods/application/route.js b/app/pods/application/route.js index e2e92a4bb..06126d0a7 100644 --- a/app/pods/application/route.js +++ b/app/pods/application/route.js @@ -1,257 +1,32 @@ import Ember from 'ember'; -import Socket from 'ui/utils/socket'; -import Util from 'ui/utils/util'; export default Ember.Route.extend({ - socket: null, - actions: { + beforeModel: function() { + var err = this.get('app.initError'); + if ( err ) + { + this.send('error',err); + } + }, + error: function(err) { this.controller.set('error',err); this.transitionTo('failWhale'); console.log('Application ' + err.stack); }, - setPageName: function(str) { - this.controller.set('pageName',str); - }, + logout: function(transition,timedOut) { + this.set('session.isLoggedIn',0); + this.set('app.afterLoginTransition', transition); + var params = {queryParams: {}}; - // Raw message from the WebSocket - wsMessage: function(/*data*/) { - //console.log('wsMessage',data); - }, - - // WebSocket connected - wsConnected: function(tries,msec) { - var msg = 'WebSocket connected'; - if (tries > 0) + if ( timedOut ) { - msg += ' (after '+ tries + ' ' + (tries === 1 ? 'try' : 'tries'); - if (msec) - { - msg += ', ' + (msec/1000) + ' sec'; - } - - msg += ')'; + params.queryParams.timedOut = true; } - console.log(msg); - }, - // WebSocket disconnected - wsDisconnected: function() { - console.log('WebSocket disconnected'); - }, - - wsPing: function() { - console.log('WebSocket ping'); - }, - - /* - agentChanged: function(change) { - if (!change || !change.data || !change.data.resource) - { - return; - } - //console.log('Agent Changed:', change); - var agent = change.data.resource; - var id = agent.id; - delete agent.hosts; - - var hosts = this.controllerFor('hosts'); - hosts.forEach(function(host) { - if ( host.get('agent.id') === id ) - { - host.get('agent').setProperties(agent); - } - }); - }, - */ - - containerChanged: function(change) { - this._instanceChanged(change); - }, - - instanceChanged: function(change) { - this._instanceChanged(change); - }, - - mountChanged: function(change) { - var mount = change.data.resource; - var volume = this.get('store').getById('volume', mount.get('volumeId')); - if ( volume ) - { - var mounts = volume.get('mounts'); - if ( !Ember.isArray('mounts') ) - { - mounts = []; - volume.set('mounts',mounts); - } - - var existingMount = mounts.filterBy('id', mount.get('id')).get('firstObject'); - if ( existingMount ) - { - existingMount.setProperties(mount); - } - else - { - mounts.pushObject(mount); - } - } + this.transitionTo('login', params); } }, - - enter: function() { - var self = this; - var store = this.get('store'); - var boundTypeify = store._typeify.bind(store); - - var socket = Socket.create({ - url: "ws://"+window.location.host + this.get('app.wsEndpoint'), - }); - - socket.on('message', function(event) { - var d = JSON.parse(event.data, boundTypeify); - self._trySend('wsMessage',d); - - var str = d.name; - if ( d.resourceType ) - { - str += ' ' + d.resourceType; - - if ( d.resourceId ) - { - str += ' ' + d.resourceId; - } - } - - var action; - if ( d.name === 'resource.change' ) - { - action = d.resourceType+'Changed'; - } - else if ( d.name === 'ping' ) - { - action = 'wsPing'; - } - - if ( action ) - { - self._trySend(action,d); - } - }); - - socket.on('connected', function(tries, after) { - self._trySend('wsConnected', tries, after); - }); - - socket.on('disconnected', function() { - self._trySend('wsDisconnected', self.get('tries')); - }); - - this.set('socket', socket); - socket.connect(); - }, - - exit: function() { - var socket = this.get('socket'); - if ( socket ) - { - socket.disconnect(); - } - }, - - _trySend: function(/*arguments*/) { - try - { - this.send.apply(this,arguments); - } - catch (err) - { - if ( err instanceof Ember.Error && err.message.indexOf('Nothing handled the action') === 0 ) - { - // Don't care - } - else - { - throw err; - } - } - }, - - - _findInCollection: function(collectionName,id) { - var collection = this.controllerFor(collectionName); - var existing = collection.filterBy('id',id).get('firstObject'); - return existing; - }, - - _instanceChanged: function(change) { - if (!change || !change.data || !change.data.resource) - { - return; - } - - //console.log('Instance Changed:',change); - var self = this; - var instance = change.data.resource; - var id = instance.get('id'); - - // All the hosts - var allHosts = self.get('store').all('host'); - - // Host IDs the instance should be on - var expectedHostIds = []; - if ( instance.get('state') !== 'purged' ) - { - expectedHostIds = (instance.get('hosts')||[]).map(function(host) { - return host.get('id'); - }); - } - - // Host IDs it is currently on - var curHostIds = []; - allHosts.forEach(function(host) { - var existing = (host.get('instances')||[]).filterBy('id', id); - if ( existing.length ) - { - curHostIds.push(host.get('id')); - } - }); - - // Remove from hosts the instance shouldn't be on - var remove = Util.arrayDiff(curHostIds, expectedHostIds); - remove.forEach(function(hostId) { - var host = self._findInCollection('hosts',hostId); - if ( host ) - { - var instances = host.get('instances'); - if ( !instances ) - { - return; - } - - instances.removeObjects(instances.filterBy('id', id)); - } - }); - - // Add or update hosts the instance should be on - expectedHostIds.forEach(function(hostId) { - var host = self._findInCollection('hosts',hostId); - if ( host ) - { - var instances = host.get('instances'); - if ( !instances ) - { - instances = []; - host.set('instances',instances); - } - - var existing = instances.filterBy('id', id); - if ( existing.length === 0) - { - instances.pushObject(instance); - } - } - }); - } }); diff --git a/app/pods/application/template.hbs b/app/pods/application/template.hbs index 9418df921..348b9def2 100644 --- a/app/pods/application/template.hbs +++ b/app/pods/application/template.hbs @@ -1,14 +1,10 @@
-
-
+
+

Loading…

{{outlet "overlay"}} {{outlet "error"}} -{{page-nav expand=navExpand}} -{{page-header pageName=pageName}} -
- {{outlet}} -
+{{outlet}} diff --git a/app/pods/application/view.js b/app/pods/application/view.js index 2a9c5b368..8645cb60b 100644 --- a/app/pods/application/view.js +++ b/app/pods/application/view.js @@ -1,8 +1,6 @@ import Ember from "ember"; export default Ember.View.extend({ - classNameBindings: ['context.navExpand:nav-expand'], - didInsertElement: function() { this.$().tooltip({ selector: '*[tooltip]', diff --git a/app/pods/authenticated/controller.js b/app/pods/authenticated/controller.js new file mode 100644 index 000000000..cba99afeb --- /dev/null +++ b/app/pods/authenticated/controller.js @@ -0,0 +1,16 @@ +import Ember from "ember"; + +export default Ember.Controller.extend({ + navExpand: (parseInt($.cookie('navExpand'),10) !== 0), + error: null, + pageName: '', + + navExpandChange: function() { + var inAYear = new Date(); + inAYear.setYear(inAYear.getFullYear()+1); + + $.cookie('navExpand', (this.get('navExpand') ? 1 : 0), { + expires: inAYear + }); + }.observes('navExpand'), +}); diff --git a/app/pods/authenticated/index/route.js b/app/pods/authenticated/index/route.js new file mode 100644 index 000000000..edeb2f088 --- /dev/null +++ b/app/pods/authenticated/index/route.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + beforeModel: function() { + this.transitionTo('hosts'); + } +}); diff --git a/app/pods/authenticated/route.js b/app/pods/authenticated/route.js new file mode 100644 index 000000000..7871be915 --- /dev/null +++ b/app/pods/authenticated/route.js @@ -0,0 +1,282 @@ +import Ember from 'ember'; +import Socket from 'ui/utils/socket'; +import Util from 'ui/utils/util'; +import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + socket: null, + + model: function() { + return this.get('store').find('schema', null, {url: 'schemas'}).then(function() { + return Ember.RSVP.resolve(); + }); + }, + + actions: { + error: function(err,transition) { + // Unauthorized error, send back to login screen + if ( err.status === 401 ) + { + this.send('logout',transition,true); + return false; + } + else + { + // Bubble up + return true; + } + }, + + setPageName: function(str) { + this.controller.set('pageName',str); + }, + + // Raw message from the WebSocket + wsMessage: function(/*data*/) { + //console.log('wsMessage',data); + }, + + // WebSocket connected + wsConnected: function(tries,msec) { + var msg = 'WebSocket connected'; + if (tries > 0) + { + msg += ' (after '+ tries + ' ' + (tries === 1 ? 'try' : 'tries'); + if (msec) + { + msg += ', ' + (msec/1000) + ' sec'; + } + + msg += ')'; + } + console.log(msg); + }, + + // WebSocket disconnected + wsDisconnected: function() { + console.log('WebSocket disconnected'); + }, + + wsPing: function() { + console.log('WebSocket ping'); + }, + + /* + agentChanged: function(change) { + if (!change || !change.data || !change.data.resource) + { + return; + } + //console.log('Agent Changed:', change); + var agent = change.data.resource; + var id = agent.id; + delete agent.hosts; + + var hosts = this.controllerFor('hosts'); + hosts.forEach(function(host) { + if ( host.get('agent.id') === id ) + { + host.get('agent').setProperties(agent); + } + }); + }, + */ + + containerChanged: function(change) { + this._instanceChanged(change); + }, + + instanceChanged: function(change) { + this._instanceChanged(change); + }, + + mountChanged: function(change) { + var mount = change.data.resource; + var volume = this.get('store').getById('volume', mount.get('volumeId')); + if ( volume ) + { + var mounts = volume.get('mounts'); + if ( !Ember.isArray('mounts') ) + { + mounts = []; + volume.set('mounts',mounts); + } + + var existingMount = mounts.filterBy('id', mount.get('id')).get('firstObject'); + if ( existingMount ) + { + existingMount.setProperties(mount); + } + else + { + mounts.pushObject(mount); + } + } + } + }, + + enter: function() { + var self = this; + var store = this.get('store'); + var boundTypeify = store._typeify.bind(store); + + var url = "ws://"+window.location.host + this.get('app.wsEndpoint'); + var token = this.get('session.token'); + if ( token ) + { + url += (url.indexOf('?') >= 0 ? '&' : '?') + 'token=' + encodeURIComponent(token); + } + + var socket = Socket.create({ + url: url + }); + + socket.on('message', function(event) { + var d = JSON.parse(event.data, boundTypeify); + self._trySend('wsMessage',d); + + var str = d.name; + if ( d.resourceType ) + { + str += ' ' + d.resourceType; + + if ( d.resourceId ) + { + str += ' ' + d.resourceId; + } + } + + var action; + if ( d.name === 'resource.change' ) + { + action = d.resourceType+'Changed'; + } + else if ( d.name === 'ping' ) + { + action = 'wsPing'; + } + + if ( action ) + { + self._trySend(action,d); + } + }); + + socket.on('connected', function(tries, after) { + self._trySend('wsConnected', tries, after); + }); + + socket.on('disconnected', function() { + self._trySend('wsDisconnected', self.get('tries')); + }); + + this.set('socket', socket); + socket.connect(); + }, + + exit: function() { + var socket = this.get('socket'); + if ( socket ) + { + socket.disconnect(); + } + + // Forget all the things + this.get('store').reset(); + }, + + _trySend: function(/*arguments*/) { + try + { + this.send.apply(this,arguments); + } + catch (err) + { + if ( err instanceof Ember.Error && err.message.indexOf('Nothing handled the action') === 0 ) + { + // Don't care + } + else + { + throw err; + } + } + }, + + + _findInCollection: function(collectionName,id) { + var collection = this.controllerFor(collectionName); + var existing = collection.filterBy('id',id).get('firstObject'); + return existing; + }, + + _instanceChanged: function(change) { + if (!change || !change.data || !change.data.resource) + { + return; + } + + //console.log('Instance Changed:',change); + var self = this; + var instance = change.data.resource; + var id = instance.get('id'); + + // All the hosts + var allHosts = self.get('store').all('host'); + + // Host IDs the instance should be on + var expectedHostIds = []; + if ( instance.get('state') !== 'purged' ) + { + expectedHostIds = (instance.get('hosts')||[]).map(function(host) { + return host.get('id'); + }); + } + + // Host IDs it is currently on + var curHostIds = []; + allHosts.forEach(function(host) { + var existing = (host.get('instances')||[]).filterBy('id', id); + if ( existing.length ) + { + curHostIds.push(host.get('id')); + } + }); + + // Remove from hosts the instance shouldn't be on + var remove = Util.arrayDiff(curHostIds, expectedHostIds); + remove.forEach(function(hostId) { + var host = self._findInCollection('hosts',hostId); + if ( host ) + { + var instances = host.get('instances'); + if ( !instances ) + { + return; + } + + instances.removeObjects(instances.filterBy('id', id)); + } + }); + + // Add or update hosts the instance should be on + expectedHostIds.forEach(function(hostId) { + var host = self._findInCollection('hosts',hostId); + if ( host ) + { + var instances = host.get('instances'); + if ( !instances ) + { + instances = []; + host.set('instances',instances); + } + + var existing = instances.filterBy('id', id); + if ( existing.length === 0) + { + instances.pushObject(instance); + } + } + }); + } +}); diff --git a/app/pods/authenticated/template.hbs b/app/pods/authenticated/template.hbs new file mode 100644 index 000000000..cb16dd14b --- /dev/null +++ b/app/pods/authenticated/template.hbs @@ -0,0 +1,5 @@ +{{page-nav expand=navExpand}} +{{page-header pageName=pageName}} +
+ {{outlet}} +
diff --git a/app/pods/authenticated/view.js b/app/pods/authenticated/view.js new file mode 100644 index 000000000..13aff1ce9 --- /dev/null +++ b/app/pods/authenticated/view.js @@ -0,0 +1,5 @@ +import Ember from "ember"; + +export default Ember.View.extend({ + classNameBindings: ['context.navExpand:nav-expand'], +}); diff --git a/app/pods/components/github-avatar/component.js b/app/pods/components/github-avatar/component.js new file mode 100644 index 000000000..29ee2eb2f --- /dev/null +++ b/app/pods/components/github-avatar/component.js @@ -0,0 +1,29 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + type: 'user', + login: null, + + classNames: ['gh-avatar'], + name: 'Loading...', + avatarUrl: null, + + nameChanged: function() { + var self = this; + var url = 'https://api.github.com/' + this.get('type') + 's/' + this.get('login') + '?s=40'; + + Ember.$.ajax({url: url, dataType: 'json'}).then(function(body) { + self.set('name', body.name); + self.set('avatarUrl', body.avatar_url); + }, function() { + var type = self.get('type'); + type = type.substr(0,1).toUpperCase() + type.substr(1); + + self.set('name', 'Warning: ' + type + ' not found'); + }); + }.observes('login','type').on('init'), + + url: function() { + return 'https://github.com/'+ encodeURIComponent(this.get('login')); + }.property('login'), +}); diff --git a/app/pods/components/github-avatar/template.hbs b/app/pods/components/github-avatar/template.hbs new file mode 100644 index 000000000..eb63daac8 --- /dev/null +++ b/app/pods/components/github-avatar/template.hbs @@ -0,0 +1,16 @@ +{{yield}} +
+ {{#if avatarUrl}} + + + + {{else}} +
+ {{/if}} +
+
+
+ {{login}} +
+
{{name}}
+
diff --git a/app/pods/components/page-header/template.hbs b/app/pods/components/page-header/template.hbs index fc2659e2f..c7acfbaf8 100644 --- a/app/pods/components/page-header/template.hbs +++ b/app/pods/components/page-header/template.hbs @@ -1,7 +1,33 @@ -
- - - - -
-

{{pageName}}

+{{#if app.hasAuthentication}} + {{#if app.authenticationEnabled}} +
+ +
+ {{else}} +
+ +
+ {{/if}} +

{{pageName}}

+{{/if}} diff --git a/app/pods/hosts/index/template.hbs b/app/pods/hosts/index/template.hbs index 62be2e7d6..4756b8111 100644 --- a/app/pods/hosts/index/template.hbs +++ b/app/pods/hosts/index/template.hbs @@ -17,4 +17,6 @@
{{/each}} +{{else}} +
No hosts or containers yet.
{{/each}} diff --git a/app/pods/hosts/route.js b/app/pods/hosts/route.js index 164e25444..55f4656dd 100644 --- a/app/pods/hosts/route.js +++ b/app/pods/hosts/route.js @@ -5,11 +5,6 @@ export default Ember.Route.extend({ return this.get('store').findAll('host'); }, - renderTemplate: function() { - this._super(); - this.render('hosts', {into: 'application'}); - }, - actions: { newContainer: function() { this.transitionTo('newContainer'); diff --git a/app/pods/login/controller.js b/app/pods/login/controller.js new file mode 100644 index 000000000..a1f3845ad --- /dev/null +++ b/app/pods/login/controller.js @@ -0,0 +1,83 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + queryParams: ['timedOut'], + + timedOut: false, + waiting: false, + errorMsg: null, + + infoColor: function() { + if ( this.get('errorMsg') ) + { + return 'alert-warning'; + } + { + return 'alert-info'; + } + }.property('errorMsg'), + + infoMsg: function() { + if ( this.get('errorMsg') ) + { + return this.get('errorMsg'); + } + else if ( this.get('timedOut') ) + { + return 'Your session has timed out. Log in again to continue.'; + } + else + { + return ''; + } + }.property('timedOut','waiting','errorMsg'), + + actions: { + authenticate: function() { + var self = this; + var session = self.get('session'); + var app = self.get('app'); + + self.set('timedOut', false); + self.set('waiting', true); + + self.get('torii').open('github-oauth2').then(function(github){ + return self.get('store').rawRequest({ + url: 'token', + method: 'POST', + data: { + code: github.authorizationCode, + } + }).then(function(res) { + var auth = JSON.parse(res.xhr.responseText); + session.set('token', auth.jwt); + session.set('isLoggedIn',1); + var transition = app.get('afterLoginTransition'); + if ( transition ) + { + app.set('afterLoginTransition', null); + transition.retry(); + } + else + { + self.transitionToRoute('index'); + } + }); + }) + .catch(function(res) { + if ( res.xhr && res.xhr.responseText ) + { + var body = JSON.parse(res.xhr.responseText); + self.set('errorMsg', body.message); + } + else + { + self.set('errorMsg', res.err); + } + }).finally(function() { + self.set('waiting', false); + }); + } + } +}); + diff --git a/app/pods/login/route.js b/app/pods/login/route.js new file mode 100644 index 000000000..f1f45be8e --- /dev/null +++ b/app/pods/login/route.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + beforeModel: function() { + if ( !this.get('app.authenticationEnabled') ) + { + this.transitionTo('index'); + } + } +}); diff --git a/app/pods/login/template.hbs b/app/pods/login/template.hbs new file mode 100644 index 000000000..e18f14b6b --- /dev/null +++ b/app/pods/login/template.hbs @@ -0,0 +1,25 @@ +
+
+
+
+
+

Welcome to Rancher

+
+ + {{#if infoMsg}} + + {{/if}} + +

Rancher uses GitHub to manage acounts and teams. Click this comically large button to log in and give Rancher read-only access to your GitHub account.

+
+ +

+ +

+
+
+
+
+
diff --git a/app/pods/login/view.js b/app/pods/login/view.js new file mode 100644 index 000000000..73e3d8775 --- /dev/null +++ b/app/pods/login/view.js @@ -0,0 +1,11 @@ +import Ember from "ember"; + +export default Ember.View.extend({ + didInsertElement: function() { + $('BODY').addClass('farm'); + }, + + willDestroyElement: function() { + $('BODY').removeClass('farm'); + }, +}); diff --git a/app/pods/logout/route.js b/app/pods/logout/route.js new file mode 100644 index 000000000..d87a29227 --- /dev/null +++ b/app/pods/logout/route.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + beforeModel: function() { + var session = this.get('session'); + session.clear(); + this.send('logout'); + } +}); diff --git a/app/pods/settings/auth/controller.js b/app/pods/settings/auth/controller.js new file mode 100644 index 000000000..d8a21afd1 --- /dev/null +++ b/app/pods/settings/auth/controller.js @@ -0,0 +1,176 @@ +import Ember from 'ember'; + +export default Ember.ObjectController.extend({ + confirmDisable: false, + error: null, + + createIncomplete: function() { + var id = (this.get('clientId')||'').trim(); + var secret = (this.get('clientSecret')||'').trim(); + return id.length < 20 ||secret.length < 40; + }.property('clientId','clientSecret'), + + destinationUrl: function() { + return window.location.origin+'/'; + }.property(), + + actions: { + test: function() { + var self = this; + + var model = this.get('model'); + model.set('clientId', model.get('clientId').trim()); + model.set('clientSecret', model.get('clientSecret').trim()); + model.set('enabled',false); // It should already be, but just in case.. + model.set('allowOrganizations',null); + model.set('allowUsers',null); + + model.save().then(function() { + self.send('authenticate'); + }).catch(function(err) { + self.send('gotError', err); + }); + }, + + authenticate: function() { + var self = this; + self.get('torii').open('github-oauth2').then(function(github){ + return self.get('store').rawRequest({ + url: 'token', + method: 'POST', + data: { + code: github.authorizationCode, + } + }).then(function(res) { + var auth = JSON.parse(res.xhr.responseText); + self.send('authenticationSucceeded', auth); + }).catch(function(err) { + // Github auth succeeded but didn't get back a token + self.send('gotError', err); + }); + }) + .catch(function(err) { + // Github auth failed.. try again + self.send('gotError', err); + }); + }, + + authenticationSucceeded: function(auth) { + console.log('Authentication succeeded'); + var self = this; + var session = self.get('session'); + + session.set('token', auth.jwt); + session.set('isLoggedIn',1); + + var model = this.get('model'); + model.set('enabled',true); + model.set('allowOrganizations', auth.orgs||[]); + model.set('allowUsers', [auth.user]); + model.save().then(function() { + self.send('waitAndRefresh', true); + }).catch(function() { + // @TODO something + }); + }, + + waitAndRefresh: function(expect,limit) { + console.log('Wait and refresh',expect,limit); + var self = this; + if ( limit === undefined ) + { + limit = 5; + } + else if ( limit === 0 ) + { + self.send('error', 'Timed out waiting for access control to turn ' + (expect ? 'on' : 'off')); + return; + } + + setTimeout(function() { + self.get('store').rawRequest({ + url: 'schemas', + headers: { 'authorization': undefined } + }).then(function() { + if ( expect === false ) + { + window.location.href = window.location.href; + } + else + { + self.send('waitAndRefresh',expect,limit-1); + } + }).catch(function() { + if ( expect === true ) + { + window.location.href = window.location.href; + } + else + { + self.send('waitAndRefresh',expect,limit-1); + } + }); + }, 5000/limit); + }, + + addUser: function() { + var str = (this.get('addUser')||'').trim(); + if ( str ) + { + this.get('allowUsers').pushObject(str); + this.set('addUser',''); + } + }, + + removeUser: function(user) { + this.get('allowUsers').removeObject(user); + }, + + addOrg: function() { + var str = (this.get('addOrg')||'').trim(); + if ( str ) + { + this.get('allowOrganizations').pushObject(str); + this.set('addOrg',''); + } + }, + + removeOrg: function(org) { + this.get('allowOrganizations').removeObject(org); + }, + + saveAuthorization: function() { + var self = this; + var model = self.get('model'); + model.save().then(function() { + self.send('waitAndRefresh', true); + }).catch(function(err) { + self.send('gotError', err); + }); + }, + + promptDisable: function() { + this.set('confirmDisable',true); + }, + + gotError: function(err) { + this.set('error', err.message); + window.scrollY = 0; + }, + + disable: function() { + var self = this; + + var model = this.get('model'); + model.set('allowOrganizations',[]); + model.set('allowUsers',[]); + model.set('enabled',false); + + model.save().then(function() { + self.send('waitAndRefresh', false); + }).catch(function(err) { + self.send('gotError', err); + }); + }, + }, +}); diff --git a/app/pods/settings/auth/route.js b/app/pods/settings/auth/route.js new file mode 100644 index 000000000..795f5e2f5 --- /dev/null +++ b/app/pods/settings/auth/route.js @@ -0,0 +1,15 @@ +import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route'; +import Ember from 'ember'; + +export default Ember.Route.extend(AuthenticatedRouteMixin,{ + model: function() { + return this.get('store').find('githubconfig').then(function(collection) { + return collection.get('firstObject'); + }); + }, + + setupController: function(controller, model) { + this._super(controller,model); + controller.set('confirmDisable',false); + } +}); diff --git a/app/pods/settings/auth/template.hbs b/app/pods/settings/auth/template.hbs new file mode 100644 index 000000000..b7ac00bb0 --- /dev/null +++ b/app/pods/settings/auth/template.hbs @@ -0,0 +1,170 @@ +
+
+

Access Control is {{#if app.authenticationEnabled}}enabled{{else}}not configured{{/if}}

+

+ {{#if app.authenticationEnabled}} + Rancher is configured to restrict access to the GitHub users and organization members below. + {{else}} + Rancher can be configured to restrict access to a set of GitHub users and organization members. This is not currently set up, so anybody that reach this page (or the API) has full control over the system. + {{/if}} +

+
+ + {{#if error}} +
+ +

{{error}}

+
+ {{/if}} + + {{#if app.authenticationEnabled}} + +
+

Configure Authorization

+
+

Configure what users and organization members should be allowed to use Rancher.

+
+
+ + +
+
+ {{input type="text" value=addUser placeholder="Add GitHub username" class="form-control"}} +
+ +
+
+
+ +
+ +
    + {{#each user in allowUsers}} +
  • + {{#github-avatar type="user" login=user}} + + {{/github-avatar}} +
  • + {{else}} + No Users + {{/each}} +
+
+ +
+ + +
+
+ {{input type="text" value=addOrg placeholder="Add GitHub organization name" class="form-control"}} +
+ +
+
+
+ +
+ +
    + {{#each org in allowOrganizations}} +
  • + {{#github-avatar type="org" login=org}} + + {{/github-avatar}} +
  • + {{else}} + No Organizations + {{/each}} +
+
+
+ +
+ + +
+ +
+

Danger Zone™

+
+ +

+ Caution: Disabling access control will give complete control over Rancher to anyone that can reach this page or the API. +

+ + {{#if confirmDisable}} + + {{else}} + + {{/if}} + +
+ {{/if}} + + {{#unless app.authenticationEnabled}} +
+

1. Setup a GitHub Application

+
+

+

    +
  • + Click here to go to your GitHub applications settings in a new window. +
  • +
  • + Click "Register new application" and fill out the form: +
      +
    • Application name: Anything you like, e.g. My Rancher
    • +
    • + Homepage URL:  {{destinationUrl}} +
    • +
    • Application description: Anything you like, optional
    • +
    • Authorization callback URL {{destinationUrl}}
    • +
    +
  • +
  • Click "Register application"
  • +
+

+
+ +
+

2. Configure Rancher to use your Application for Authentication

+
+

Copy and paste the Client ID and Secret from the upper-right corner of your newly-created application.

+
+
+
+ + {{input id="client-id" type="text" value=clientId classNames="form-control"}} +
+
+
+
+ + {{input id="client-secret" type="text" value=clientSecret classNames="form-control"}} +
+
+
+
+ +
+

3. Test and enable Authentication

+
+
+
+

Check that your application is configured correctly by testing authentication with it:

+
+
+ +
+
+
+ {{/unless}} +
diff --git a/app/pods/settings/auth/view.js b/app/pods/settings/auth/view.js new file mode 100644 index 000000000..4c0e6cab4 --- /dev/null +++ b/app/pods/settings/auth/view.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export default Ember.View.extend({ + didInsertElement: function() { + $('#authEnabled').bootstrapSwitch(); + }, +}); diff --git a/app/pods/settings/route.js b/app/pods/settings/route.js new file mode 100644 index 000000000..26d9f3124 --- /dev/null +++ b/app/pods/settings/route.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ +}); diff --git a/app/pods/settings/template.hbs b/app/pods/settings/template.hbs new file mode 100644 index 000000000..c24cd6895 --- /dev/null +++ b/app/pods/settings/template.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/app/pods/volumes/route.js b/app/pods/volumes/route.js index dde249f92..38b029ef7 100644 --- a/app/pods/volumes/route.js +++ b/app/pods/volumes/route.js @@ -4,9 +4,4 @@ export default Ember.Route.extend({ model: function() { return this.get('store').findAll('volume'); }, - - renderTemplate: function() { - this.render('volumes', {into: 'application'}); - }, - }); diff --git a/app/router.js b/app/router.js index 8d04f9c05..f70f10b1c 100644 --- a/app/router.js +++ b/app/router.js @@ -6,33 +6,43 @@ var Router = Ember.Router.extend({ }); Router.map(function() { - this.route('failWhale', { path: '/failwhale' }); + this.route('index'); + this.route('failWhale', { path: '/fail' }); - this.resource('hosts', { path: '/hosts'}, function() { - this.resource('host', { path: '/:host_id' }, function() { - this.route('delete'); + this.route('login'); + this.route('logout'); + this.route('authenticated', { path: '/'}, function() { + this.resource('settings', function() { + this.route('auth'); }); - this.route('containerNew', {path: '/containers/new'}); - this.resource('container', { path: '/containers/:container_id' }, function() { - this.route('shell'); - this.route('edit'); - this.route('delete'); - }); - }); + this.resource('hosts', { path: '/hosts'}, function() { + this.resource('host', { path: '/:host_id' }, function() { + this.route('delete'); + }); - this.resource('apikeys', {path: '/api'}, function() { - this.route('new', {path: '/api/new'}); - this.resource('apikey', {path: '/:apikey_id'}, function() { - this.route('edit'); - this.route('delete'); + this.route('containerNew', {path: '/containers/new'}); + this.resource('container', { path: '/containers/:container_id' }, function() { + this.route('shell'); + this.route('edit'); + this.route('delete'); + }); }); - }); - this.resource('volumes', {path: '/volumes'}, function() { - this.resource('volume', {path: '/:volume_id'}, function() { - this.route('delete'); + this.resource('apikeys', {path: '/api'}, function() { + this.route('new', {path: '/api/new'}); + this.resource('apikey', {path: '/:apikey_id'}, function() { + this.route('edit'); + this.route('delete'); + }); }); + + this.resource('volumes', {path: '/volumes'}, function() { + this.resource('volume', {path: '/:volume_id'}, function() { + this.route('delete'); + }); + }); + }); }); diff --git a/app/routes/fail-whale.js b/app/routes/fail-whale.js index 34359bfb6..b54ac251d 100644 --- a/app/routes/fail-whale.js +++ b/app/routes/fail-whale.js @@ -1,6 +1,14 @@ import Ember from 'ember'; export default Ember.Route.extend({ + enter: function() { + $('BODY').addClass('farm'); + }, + + exit: function() { + $('BODY').removeClass('farm'); + }, + model: function() { return this.controllerFor('application').get('error'); }, @@ -8,7 +16,7 @@ export default Ember.Route.extend({ afterModel: function(model) { if ( !model ) { - this.transitionTo('hosts'); +// this.transitionTo('hosts'); } } }); diff --git a/app/routes/index.js b/app/routes/index.js index 0321a5ba6..c0bb853f4 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -3,5 +3,5 @@ import Ember from 'ember'; export default Ember.Route.extend({ enter: function() { this.transitionTo('hosts'); - } + }, }); diff --git a/app/routes/loading.js b/app/routes/loading.js index 3579ca688..031e2d253 100644 --- a/app/routes/loading.js +++ b/app/routes/loading.js @@ -6,7 +6,7 @@ export default Ember.Route.extend({ Ember.run(function() { $('#loading-underlay').show().fadeIn({duration: 100, queue: false, easing: 'linear', complete: function() { - $('#loading-overlay').fadeIn({duration: 200, queue: false, easing: 'linear'}); + $('#loading-overlay').show().fadeIn({duration: 200, queue: false, easing: 'linear'}); }}); }); }, diff --git a/app/styles/app.scss b/app/styles/app.scss index e4c9f34a3..a643a0052 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -10,3 +10,4 @@ @import "app/styles/overlay"; @import "app/styles/progress-bar"; @import "app/styles/host"; +@import "app/styles/github-avatar"; diff --git a/app/styles/github-avatar.scss b/app/styles/github-avatar.scss new file mode 100644 index 000000000..a7ea05a5e --- /dev/null +++ b/app/styles/github-avatar.scss @@ -0,0 +1,43 @@ +.gh-avatar-list { + margin-bottom: 0; + + LI { + margin-bottom: 5px; + + &:last-of-type { + margin-bottom: 0; + } + } +} + +.gh-avatar { + IMG { + border-radius: 3px; + } + + .gh-icon { + float: left; + margin-right: 10px; + width: 40px; + height: 40px; + } + + .gh-placeholder { + width: 40px; + height: 40px; + border: 1px solid #aaa; + border-radius: 3px; + } + + .gh-avatar-content { + margin-left: 50px; + } + + .gh-action { + display: none; + } + + &:hover .gh-action { + display: block; + } +} diff --git a/app/styles/layout.scss b/app/styles/layout.scss index 8c2f5125c..831e9cafa 100644 --- a/app/styles/layout.scss +++ b/app/styles/layout.scss @@ -5,6 +5,13 @@ BODY { background-color: #eceff4; } +.farm { + background-color: #85a514; + background-image: url('images/background.jpg'); + background-position: top center; + background-repeat: no-repeat; +} + /********** * Navigation (left side bar) **********/ @@ -109,12 +116,13 @@ HEADER { z-index: 2; margin-left: $nav_width; background-color: $header_bg; - padding: 0 20px; + padding: 0; H2 { font-weight: normal; margin: 0; line-height: $header_height; + padding-left: 20px; } } @@ -247,23 +255,26 @@ TABLE.graphs { table-layout: fixed; } -.fail-whale { - background-color: #85a514; - background-image: url('images/background.jpg'); - background-position: top center; - background-repeat: no-repeat; - position: absolute; +.farm-box { top: 0; left: 0; right: 0; bottom: 0; - z-index: 999; SECTION { - width: 50%; - margin: 50px auto 0 auto; + min-width: 50%; + margin: 20px auto 0 auto; background-color: white; border-radius: 5px; - padding: 30px; + padding: 20px; + } + + SECTION:first-of-type { + margin-top: 50px; } } + +.hand { + cursor: pointer; + cursor: hand; +} diff --git a/app/templates/fail-whale.hbs b/app/templates/fail-whale.hbs index f3aa84175..191b9765a 100644 --- a/app/templates/fail-whale.hbs +++ b/app/templates/fail-whale.hbs @@ -1,10 +1,16 @@ -
-
-

Error

-
{{code}} {{#if status}}({{status}}){{/if}}
-
{{message}}
-
- Reload to try again +
+
+
+
+
+

Error

+
{{code}} {{#if status}}({{status}}){{/if}}
+
{{message}}
+
+ Reload to try again +
+
-
+
+
diff --git a/app/utils/local-storage.js b/app/utils/local-storage.js new file mode 100644 index 000000000..028a5c534 --- /dev/null +++ b/app/utils/local-storage.js @@ -0,0 +1,35 @@ +import Ember from "ember"; + +export default Ember.Object.extend({ + unknownProperty: function(key) { + return localStorage[key]; + }, + + setUnknownProperty: function(key, value) { + if( Ember.isNone(value) ) + { + delete localStorage[key]; + } + else + { + localStorage[key] = value; + } + + this.notifyPropertyChange(key); + return value; + }, + + clear: function() { + var i; + + this.beginPropertyChanges(); + for ( i = 0 ; i < localStorage.length ; i++ ) + { + this.set(localStorage.key(i)); + } + + localStorage.clear(); + this.endPropertyChanges(); + } +}); + diff --git a/app/utils/session-storage.js b/app/utils/session-storage.js new file mode 100644 index 000000000..50fb11d91 --- /dev/null +++ b/app/utils/session-storage.js @@ -0,0 +1,35 @@ +import Ember from "ember"; + +export default Ember.Object.extend({ + unknownProperty: function(key) { + return sessionStorage[key]; + }, + + setUnknownProperty: function(key, value) { + if( Ember.isNone(value) ) + { + delete sessionStorage[key]; + } + else + { + sessionStorage[key] = value; + } + + this.notifyPropertyChange(key); + return value; + }, + + clear: function() { + var i; + + this.beginPropertyChanges(); + for ( i = 0 ; i < sessionStorage.length ; i++ ) + { + this.set(sessionStorage.key(i)); + } + + sessionStorage.clear(); + this.endPropertyChanges(); + } +}); + diff --git a/blueprints/route/files/app/__path__/__name__.js b/blueprints/route/files/app/__path__/__name__.js index 6052d9bac..72cf6cbb1 100644 --- a/blueprints/route/files/app/__path__/__name__.js +++ b/blueprints/route/files/app/__path__/__name__.js @@ -1,4 +1,4 @@ -import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin'; +import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route'; import Ember from 'ember'; export default Ember.Route.extend(AuthenticatedRouteMixin, { diff --git a/bower.json b/bower.json index c2ed64785..69bc0c381 100644 --- a/bower.json +++ b/bower.json @@ -19,6 +19,7 @@ "c3": "~0.4.3", "jquery.cookie": "~1.4.1", "bootstrap-sass-official": "~3.3.1", - "bootstrap-multiselect": "~0.9.10" + "bootstrap-multiselect": "~0.9.10", + "bootstrap-switch": "~3.3.0" } } diff --git a/config/environment.js b/config/environment.js index 0bcee32f7..089b2c9d8 100644 --- a/config/environment.js +++ b/config/environment.js @@ -16,12 +16,17 @@ module.exports = function(environment) { } }, + torii: { + // This is configured at runtime, but torii complains + // on startup if there's no entry in the environment + }, + contentSecurityPolicy: { // Allow the occasional ... 'style-src': "'self' cdn.rancher.io 'unsafe-inline'", 'font-src': "'self' cdn.rancher.io", 'script-src': "'self' cdn.rancher.io", - 'img-src': "'self' cdn.rancher.io", + 'img-src': "'self' cdn.rancher.io avatars.githubusercontent.com", // Allow connect to anywhere, for console and event stream socket 'connect-src': '*' @@ -43,7 +48,7 @@ module.exports = function(environment) { // ENV.APP.LOG_RESOLVER = true; ENV.APP.LOG_ACTIVE_GENERATION = true; ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + ENV.APP.LOG_TRANSITIONS_INTERNAL = true; ENV.APP.LOG_VIEW_LOOKUPS = true; ENV.contentSecurityPolicy['script-src'] = ENV.contentSecurityPolicy['script-src'] + " 'unsafe-eval'"; } diff --git a/package.json b/package.json index de3c449a0..1783a8d5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "0.7.4", + "version": "0.8.0", "private": true, "directories": { "doc": "doc", @@ -18,11 +18,12 @@ "author": "Rancher Labs", "license": "Apache 2", "devDependencies": { + "ember-api-store": "1.0.7", + "body-parser": "^1.2.0", "broccoli-asset-rev": "^1.0.0", "broccoli-sass": "^0.3.3", "connect-restreamer": "^1.0.1", - "ember-api-store": "^1.0.6", "ember-cli": "^0.1.5", "ember-cli-content-security-policy": "0.3.0", "ember-cli-dependency-checker": "0.0.7", @@ -37,6 +38,7 @@ "express": "^4.8.5", "forever-agent": "^0.5.2", "glob": "^4.0.5", - "http-proxy": "^1.6.2" + "http-proxy": "^1.6.2", + "torii": "^0.2.2" } } diff --git a/server/proxies/api.js b/server/proxies/api.js index 877760347..7ddb2bf35 100644 --- a/server/proxies/api.js +++ b/server/proxies/api.js @@ -26,7 +26,7 @@ module.exports = function(app, options) { }); proxy.on('error', function onProxyError(err, req, res) { - console.log('Proxy Error:', err); + console.log('Proxy Error: on', req.method,'to', req.url,':', err); var error = { type: 'error', status: 500, diff --git a/tests/unit/pods/authenticated/route-test.js b/tests/unit/pods/authenticated/route-test.js new file mode 100644 index 000000000..e830fd9bf --- /dev/null +++ b/tests/unit/pods/authenticated/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:authenticated', 'AuthenticatedRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +}); diff --git a/tests/unit/pods/login/route-test.js b/tests/unit/pods/login/route-test.js new file mode 100644 index 000000000..e2990c94d --- /dev/null +++ b/tests/unit/pods/login/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:login', 'LoginRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +}); diff --git a/tests/unit/pods/logout/route-test.js b/tests/unit/pods/logout/route-test.js new file mode 100644 index 000000000..c82ce8803 --- /dev/null +++ b/tests/unit/pods/logout/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:logout', 'LogoutRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +}); diff --git a/tests/unit/pods/settings/auth/route-test.js b/tests/unit/pods/settings/auth/route-test.js new file mode 100644 index 000000000..bf7468da8 --- /dev/null +++ b/tests/unit/pods/settings/auth/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:settings/auth', 'SettingsAuthRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +}); diff --git a/tests/unit/pods/settings/route-test.js b/tests/unit/pods/settings/route-test.js new file mode 100644 index 000000000..da9dcfdab --- /dev/null +++ b/tests/unit/pods/settings/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:settings', 'SettingsRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +});