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 @@
-
-
+
+
{{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}}
+
+
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}}
+
+
+
+ Access Control is not configured
+ {{#link-to "settings.auth"}}Settings{{/link-to}}
+
+
+ {{/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}}
+ {{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.
+
+
+
+
+ Authenticate with GitHub
+
+
+
+
+
+
+
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}}
+
+ {{/if}}
+
+ {{#if app.authenticationEnabled}}
+
+
+
Configure Authorization
+
+
Configure what users and organization members should be allowed to use Rancher.
+
+
+
Users
+
+
+
+
+
+
+ {{#each user in allowUsers}}
+
+ {{#github-avatar type="user" login=user}}
+ Remove
+ {{/github-avatar}}
+
+ {{else}}
+ No Users
+ {{/each}}
+
+
+
+
+
Organizations
+
+
+
+
+
+
+ {{#each org in allowOrganizations}}
+
+ {{#github-avatar type="org" login=org}}
+ Remove
+ {{/github-avatar}}
+
+ {{else}}
+ No Organizations
+ {{/each}}
+
+
+
+
+
+
+
+ Save Authorization Configuration
+
+
+
+
+
Danger Zone™
+
+
+
+ Caution: Disabling access control will give complete control over Rancher to anyone that can reach this page or the API.
+
+
+ {{#if confirmDisable}}
+
+ Are you sure? Click again to really disable Access Control
+
+ {{else}}
+
+ Disable Access Control
+
+ {{/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.
+
+
+
+ Client ID
+ {{input id="client-id" type="text" value=clientId classNames="form-control"}}
+
+
+
+
+ Client Secret
+ {{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:
+
+
+
+ Authenticate with GitHub
+
+
+
+
+ {{/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}}
+
+
-
+
+
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);
+});