You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2015/02/02 08:32:12 UTC

[1/2] fauxton commit: updated refs/heads/master to ddef3d5

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 9f26ac1e2 -> ddef3d54c


Initial add CORS to Fauxton


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/a9636bdc
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/a9636bdc
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/a9636bdc

Branch: refs/heads/master
Commit: a9636bdcdd7ccabb868c678d9be0ac339df282e0
Parents: 9f26ac1
Author: Christian Hogan <gi...@infliction.org>
Authored: Fri Nov 28 13:19:13 2014 -0500
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Feb 2 09:30:46 2015 +0200

----------------------------------------------------------------------
 app/addons/config/assets/less/config.less       |   4 +
 app/addons/config/routes.js                     |  33 +-
 app/addons/config/templates/dashboard.html      |   2 +-
 app/addons/config/templates/sidebartabs.html    |  10 +
 app/addons/config/views.js                      |  24 ++
 app/addons/cors/assets/less/cors.less           | 126 +++++++
 app/addons/cors/base.js                         |  24 ++
 app/addons/cors/resources.js                    |  64 ++++
 app/addons/cors/routes.js                       |  21 ++
 app/addons/cors/templates/cors.html             |  50 +++
 .../cors/templates/origin_domain_row.html       |  33 ++
 .../cors/templates/origin_domain_table.html     |  17 +
 app/addons/cors/views.js                        | 337 +++++++++++++++++++
 settings.json.default                           |   1 +
 14 files changed, 741 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/assets/less/config.less
----------------------------------------------------------------------
diff --git a/app/addons/config/assets/less/config.less b/app/addons/config/assets/less/config.less
index 6cd0ed2..fd496c6 100644
--- a/app/addons/config/assets/less/config.less
+++ b/app/addons/config/assets/less/config.less
@@ -41,6 +41,10 @@
 }
 
 table.config {
+  thead th {
+    border-left: none;
+    border-bottom: 2px solid #999;
+  }
   tr {
     th, td {
       vertical-align: middle;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 62fd9b1..32dc4cc 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -14,16 +14,33 @@ define([
   'app',
   'api',
   'addons/config/resources',
-  'addons/config/views'
+  'addons/config/views',
+  'addons/cors/views'
 ],
 
-function(app, FauxtonAPI, Config, Views) {
+function(app, FauxtonAPI, Config, Views, CORS) {
 
   var ConfigRouteObject = FauxtonAPI.RouteObject.extend({
-    layout: 'one_pane',
+    layout: 'with_tabs_sidebar',
 
     initialize: function () {
       this.configs = new Config.Collection();
+      this.cors = new CORS.config();
+      
+      this.sidebar = this.setView("#sidebar-content", new Views.Tabs({
+        sidebarItems: [
+          {
+            title: 'Main config',
+            typeSelect: 'main',
+            link: '_config'
+          },
+          {
+            title: 'CORS',
+            typeSelect: 'cors',
+            link: '_config/cors'
+          }
+        ]
+      }));
     },
 
     roles: ['_admin'],
@@ -38,12 +55,20 @@ function(app, FauxtonAPI, Config, Views) {
     },
 
     routes: {
-      '_config': 'config'
+      '_config': 'config',
+      '_config/cors':'configCORS'
     },
 
     config: function () {
       this.newSection = this.setView('#right-header', new Views.ConfigHeader({ collection: this.configs }));
       this.setView('#dashboard-content', new Views.Table({ collection: this.configs }));
+      this.sidebar.setSelectedTab("main");
+    },
+    
+    configCORS: function() {
+      this.removeView('#right-header');
+      this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSMain({ model: this.cors }));
+      this.sidebar.setSelectedTab("cors");
     },
 
     establish: function () {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/templates/dashboard.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/dashboard.html b/app/addons/config/templates/dashboard.html
index f7fae00..0af857d 100644
--- a/app/addons/config/templates/dashboard.html
+++ b/app/addons/config/templates/dashboard.html
@@ -14,7 +14,7 @@ the License.
 
 <table class="config table table-striped table-bordered">
   <thead>
-    <th id="config-section" width="20%">Section</th>
+    <th id="config-section" width="22%">Section</th>
     <th id="config-option" width="20%">Option</th>
     <th id="config-value">Value</th>
     <th id="config-trash"></th>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/templates/sidebartabs.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/sidebartabs.html b/app/addons/config/templates/sidebartabs.html
new file mode 100644
index 0000000..b48b0ae
--- /dev/null
+++ b/app/addons/config/templates/sidebartabs.html
@@ -0,0 +1,10 @@
+<ul class="nav nav-list">
+  <% _.each(sidebarItems, function (item) { %>
+    <li>
+    <a data-type-select="<%- item.typeSelect %>" href="#<%- item.link %>">
+      <span class="<%- item.icon %>"></span>
+      <%- item.title %>
+    </a>
+    </li>
+  <% }); %>
+</ul>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/views.js
----------------------------------------------------------------------
diff --git a/app/addons/config/views.js b/app/addons/config/views.js
index 034205a..1ccdc3e 100644
--- a/app/addons/config/views.js
+++ b/app/addons/config/views.js
@@ -243,6 +243,30 @@ function(app, FauxtonAPI, Config, Components) {
       });
     }
   });
+  
+  Views.Tabs = FauxtonAPI.View.extend({
+    className: "sidenav",
+    tagName: "nav",
+    template: 'addons/config/templates/sidebartabs',
+    initialize: function (options) {
+      this.sidebarItems = options.sidebarItems;
+    },
+
+    setSelectedTab: function (selectedTab) {
+      this.selectedTab = selectedTab;
+      this.$('li').removeClass('active');
+      this.$('a[data-type-select="'+this.selectedTab+'"]').parent("li").addClass('active');
+    },
+    afterRender: function(){
+      this.setSelectedTab(this.selectedTab);
+    },
+
+    serialize: function () {
+      return {
+        sidebarItems: this.sidebarItems
+      };
+    }
+  });
 
   return Views;
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/assets/less/cors.less
----------------------------------------------------------------------
diff --git a/app/addons/cors/assets/less/cors.less b/app/addons/cors/assets/less/cors.less
new file mode 100644
index 0000000..4e06032
--- /dev/null
+++ b/app/addons/cors/assets/less/cors.less
@@ -0,0 +1,126 @@
+/*  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ *  use this file except in compliance with the License. You may obtain a copy of
+ *  the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ *  License for the specific language governing permissions and limitations under
+ *  the License.
+ */
+
+/* =cors
+   ---------------------------------------------------------------------- */
+@import "../../../../../assets/less/bootstrap/variables.less";
+@import "../../../../../assets/less/variables.less";
+@import "../../../../../assets/less/bootstrap/mixins.less";
+
+#sidebar-content .nav-list li a span {
+  display: none;
+}
+
+#cors-header {
+  margin: 18px 30px 0 30px;
+}
+
+#corsForm {
+  margin: 0;
+
+  .checkbox {
+    font-size: 16px;
+
+    input[type='checkbox'] {
+      margin-top: 4px;
+    }
+  }
+  .radio {
+    font-size: 16px;
+
+    input[type='radio'] {
+      margin-top: 4px;
+    }
+  }
+
+  .cors-enable {
+    padding: 10px 30px 18px 30px;
+    border-bottom: 1px solid #ccc;
+  }
+  #origin-domains-container {
+    display: none;
+    
+    .new-origin-domain {
+      width: 100%;
+      border-radius: 6px;
+    }
+  }
+  #collapsing-container {
+    border-top: 1px solid #fff;
+    border-bottom: 1px solid #ccc;
+    padding: 18px 30px;
+    display: none;
+  }
+  .origin-domains {
+    margin: 0;
+    padding: 18px 0px;
+    
+    p {
+      line-height: 36px;
+    }
+    .controls {
+      margin-top: 18px;
+    }
+  }
+  .localhost-tip {
+    padding: 18px 30px;
+    border-top: 1px solid #fff;
+    border-bottom: 1px solid #ccc;
+  }
+  .form-actions {
+    padding: 18px 30px;
+    margin-top: 0;
+    border-top: 1px solid #fff;
+  }
+}
+
+#origin-domain-table {
+  td {
+    padding: 8px 2px;
+  }
+  span {
+    cursor: pointer;
+    color: #E33F3B;
+    display: inline-block;
+    padding-top: 9px;
+  }
+
+  .edit-domain-section {
+    position: relative;
+    margin-right: 20px;
+
+    .field-wrapper {
+      margin-right: 78px;
+    }
+    input {
+      width: 100%;
+      padding: 9px;
+      margin: 0 0 0 4px;
+    }
+  }
+  .url-display {
+    padding: 10px;
+  }
+
+  .hide {
+    display: none;
+  }
+
+  .update-domain-btn {
+    position: absolute;
+    right: -2px;
+    top: 0;
+    padding: 9px;
+    border-radius: 0 6px 6px 0;
+  }
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/base.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/base.js b/app/addons/cors/base.js
new file mode 100644
index 0000000..b5824e3
--- /dev/null
+++ b/app/addons/cors/base.js
@@ -0,0 +1,24 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  "app",
+  "api",
+  "addons/cors/routes"
+],
+
+function (app, FauxtonAPI, CORS) {
+
+  CORS.initialize = function() {};
+
+  return CORS;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/resources.js b/app/addons/cors/resources.js
new file mode 100644
index 0000000..bd3809e
--- /dev/null
+++ b/app/addons/cors/resources.js
@@ -0,0 +1,64 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  "app",
+  "api"
+],
+
+function (app, FauxtonAPI) {  
+  var CORS = FauxtonAPI.addon();
+
+
+  CORS.config = FauxtonAPI.Model.extend({
+    url: function() {
+      return app.host+"/_config/cors";
+    }
+  });
+  
+  CORS.ConfigModel = Backbone.Model.extend({
+    documentation: "cors",
+    
+    url: function () {
+      return app.host + '/_config/' + encodeURIComponent(this.get("section")) + '/' + encodeURIComponent(this.get("attribute"));
+    },
+    
+    isNew: function () { return false; },
+    
+    sync: function (method, model, options) {
+
+      var params = {
+        url: model.url(),
+        contentType: 'application/json',
+        dataType: 'json',
+        data: JSON.stringify(model.get('value'))
+      };
+
+      if (method === 'delete') {
+        params.type = 'DELETE';
+      } else {
+        params.type = 'PUT';
+      }
+
+      return $.ajax(params);
+    }
+  
+  });
+  
+    // simple helper function to validate the user entered a valid domain starting with http(s) and
+  // not including any subfolder
+  CORS.validateCORSDomain = function (str) {
+    return (/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$/).test(str);
+  };
+
+  return CORS;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/routes.js b/app/addons/cors/routes.js
new file mode 100644
index 0000000..017211b
--- /dev/null
+++ b/app/addons/cors/routes.js
@@ -0,0 +1,21 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  'app',
+  'api',
+  'addons/cors/views'
+],
+
+function (app, FauxtonAPI, CORS) {
+  return CORS;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/cors.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/cors.html b/app/addons/cors/templates/cors.html
new file mode 100644
index 0000000..8db6a60
--- /dev/null
+++ b/app/addons/cors/templates/cors.html
@@ -0,0 +1,50 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+
+<header id="cors-header">
+  <p>Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.</p>
+</header>
+
+<form id="corsForm">
+
+  <div class="cors-enable">
+    <label class="checkbox">
+      <input type="checkbox" class="js-enable-cors" name="enable_cors" <% if  (typeof enableCors !== 'undefined' && enableCors === 'true') { %> checked="checked" <% } %>> Enable CORS
+    </label>
+  </div>
+  
+  <div id="collapsing-container">
+    <p><strong>Origin Domains</strong></p>
+    
+    <p>Databases will accept requests from these domains:</p>
+    
+    <label class="checkbox"><input type="checkbox" class="js-all-origin-domains" name="all_origin_domains"> All origin domains ( * )</label></li>
+        
+    <label class="checkbox"><input type="checkbox" class="js-restrict-origin-domains" name="restrict_origin_domains"> Restrict to specific origin domains</label>
+      
+    <div id="origin-domains-container">
+      <div class="origin-domains"></div>
+      <input type="text" class="new-origin-domain" name="new_origin_domain" value="" placeholder="e.g., https://site.com" />
+    </div>
+    
+    </div>
+    
+  </div>
+
+  <div class="form-actions">
+    <input class="btn btn-success" type="submit" value="Save" />
+  </div>
+
+</form>
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/origin_domain_row.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_row.html b/app/addons/cors/templates/origin_domain_row.html
new file mode 100644
index 0000000..19f6954
--- /dev/null
+++ b/app/addons/cors/templates/origin_domain_row.html
@@ -0,0 +1,33 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+
+<td>
+  <div class="js-url url-display"><%-url%></div>
+
+  <div class="js-edit-domain edit-domain-section hide">
+    <div class="field-wrapper">
+      <input type="text" value="<%-url%>" />
+    </div>
+    <a class="js-save-domain btn update-domain-btn">
+      <i class="fonticon-save"></i>
+      Update
+    </a>
+  </div>
+</td>
+<td width="30">
+  <span class="js-edit fonticon-pencil" title="Click to edit"></span>
+</td>
+<td width="30">
+  <span class="js-delete fonticon-trash" title="Click to delete"></span>
+</td>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/origin_domain_table.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_table.html b/app/addons/cors/templates/origin_domain_table.html
new file mode 100644
index 0000000..7192bea
--- /dev/null
+++ b/app/addons/cors/templates/origin_domain_table.html
@@ -0,0 +1,17 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+
+<table id="origin-domain-table" class="table table-striped">
+  <tbody></tbody>
+</table>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/views.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/views.js b/app/addons/cors/views.js
new file mode 100644
index 0000000..c390d5a
--- /dev/null
+++ b/app/addons/cors/views.js
@@ -0,0 +1,337 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  "app",
+  "api",
+  "addons/cors/resources"
+],
+
+
+function (app, FauxtonAPI, CORS) {
+  var Views= {};
+  
+  Views.CORSMain = FauxtonAPI.View.extend({
+    className: 'cors-page',
+    template: 'addons/cors/templates/cors',
+    events: {
+      'submit form#corsForm': 'submit',
+      'click .js-enable-cors': 'corsClick',
+      'click .js-restrict-origin-domains': 'restrictOrigins',
+      'click .js-all-origin-domains': 'allOrigins'
+    },
+    
+    initialize: function () {
+      this.originDomainTable = this.setView('.origin-domains', new Views.OriginDomainTable({
+        model: this.model
+      }));
+    },
+    
+    serialize: function () {
+      return {
+        enableCors: this.model.get('credentials')    
+      };
+    },
+    
+    establish: function(){
+      return [this.model.fetch()];
+    },
+    
+    afterRender: function () {        
+      var corsEnabled = this.$('.js-enable-cors').is(':checked');
+      this.$('#collapsing-container').toggle(corsEnabled);
+      this.setupOrigins();
+    },
+    
+    corsClick: function (e) {
+      var isChecked = this.$(e.target).prop('checked');
+      this.$('#collapsing-container').toggle(isChecked);
+      this.setupOrigins();
+    },
+    
+    
+    setupOrigins: function() {
+      var storedOrigins = this.model.get('origins');
+      if (storedOrigins && storedOrigins != '*') {
+        this.restrictOrigins();
+      } else {
+        this.allOrigins();
+      }
+    },
+    
+    allOrigins: function() {
+      this.$('.js-all-origin-domains').prop('checked', true);
+      this.$('.js-restrict-origin-domains').prop('checked', false);
+      this.$('#origin-domains-container').hide();
+    },
+
+    restrictOrigins: function() {
+      this.$('.js-restrict-origin-domains').prop('checked', true);
+      this.$('.js-all-origin-domains').prop('checked', false);
+      this.$('#origin-domains-container').show();
+    },
+    
+    formToJSON: function(formSelector){
+      var formObject = $(formSelector).serializeArray(),
+        formJSON={};
+      _.map(formObject, function(field){
+        formJSON[field.name]=field.value;
+      });
+      return formJSON;
+    },
+    
+    submit: function(e){
+      e.preventDefault();      
+      var data = this.formToJSON(e.currentTarget);
+
+      if (data.enable_cors === 'on') {
+
+        // CORS checked, save data  
+        if (data.restrict_origin_domains === 'on') {
+          var storedOrigins = this.model.get('origins').split(',');
+          var newDomain = $.trim(data.new_origin_domain);
+	
+          // if a new domain has been entered, check it's valid
+          if (!_.isEmpty(newDomain) && !CORS.validateCORSDomain(newDomain)) {
+            FauxtonAPI.addNotification({
+              msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
+              type: 'error',
+              clear: true
+            });
+            return;
+          }
+          
+          // check that the user has entered at least one new origin domain
+          if (storedOrigins && storedOrigins.length > 0 && storedOrigins !== '*') {
+            this.originData = storedOrigins.concat(newDomain).toString();            
+          } else {
+            if (_.isEmpty(newDomain)) {
+              FauxtonAPI.addNotification({
+                msg: 'Please enter a new origin domain.',
+                type: 'error',
+                clear: true
+              });
+              this.$('.new-origin-domain').focus();
+              return;
+            }
+            this.originData = data.new_origin_domain;
+          }
+          
+        } else {
+          this.originData = "*";
+        }
+        
+          
+        var enableOption = new CORS.ConfigModel({
+          section: 'httpd',
+          attribute: 'enable_cors',
+          value: 'true'
+        });
+        
+        var enableCreds = new CORS.ConfigModel({
+          section: 'cors',
+          attribute: 'credentials',
+          value: 'true'
+        });
+        
+        var allowOrigins = new CORS.ConfigModel({
+          section: 'cors',
+          attribute: 'origins',
+          value: this.originData
+        });
+
+        enableOption.save().then(function (response) {
+          var notification = FauxtonAPI.addNotification({
+            msg: 'Your settings have been saved.',
+            type: 'success',
+            clear: true
+          });
+        },
+        function (response, errorCode, errorMsg) {
+          var notification = FauxtonAPI.addNotification({
+            msg: 'Sorry! There was an error. Code ' + errorCode  + '.',
+            type: 'error',
+            clear: true
+          });
+        });
+        
+        enableCreds.save();
+        allowOrigins.save();
+        this.$('.new-origin-domain').val('');
+      
+      } else {
+          
+        // Disable CORS
+        var disableOption = new CORS.ConfigModel({
+          section: 'httpd',
+          attribute: 'enable_cors',
+          value: 'false'
+        });
+        
+        var disableCreds = new CORS.ConfigModel({
+          section: 'cors',
+          attribute: 'credentials',
+          value: 'false'
+        });
+        
+        var disableOrigins = new CORS.ConfigModel({
+          section: 'cors',
+          attribute: 'origins',
+          value: ''
+        });
+        
+        disableOption.save().then(function (response) {
+          var notification = FauxtonAPI.addNotification({
+            msg: 'Your settings have been saved.',
+            type: 'success',
+            clear: true
+          });
+        },
+        function (response, errorCode, errorMsg) {
+          var notification = FauxtonAPI.addNotification({
+            msg: 'Sorry! There was an error. Code ' + errorCode  + '.',
+            type: 'error',
+            clear: true
+          });
+        });
+          
+        disableCreds.save();
+        disableOrigins.save();
+      }
+    }
+  });
+  
+  Views.OriginDomainTable = FauxtonAPI.View.extend({
+    template: 'addons/cors/templates/origin_domain_table',
+
+    initialize: function () {
+      // listen for any server-side changes to the object (i.e. saves/deletes). Only then, re-render the table
+      this.listenTo(this.model, 'sync', this.render);
+    },
+
+    beforeRender: function () {
+      var origins = this.model.get('origins');
+
+      // if the stored origins are set to '*' or nothing's defined, show nothing
+      if (_.isEmpty(origins) || origins === '*') {
+        return;
+      }
+      this.showRows();
+    },
+
+    showRows: function () {
+        var originsArray = this.model.get('origins').split(',');
+      _.each(originsArray, function (url, index) {
+        this.insertView('#origin-domain-table tbody', new Views.OriginDomainRow({
+          model: this.model,
+          index: index
+        }));
+      }, this);
+    }
+  });
+
+
+  // this gets passed the entire model so it can manipulate it directly (add/update the row)
+  Views.OriginDomainRow = FauxtonAPI.View.extend({
+    template: 'addons/cors/templates/origin_domain_row',
+    tagName: 'tr',
+
+    events: {
+      'click .js-edit': 'onEditDomain',
+      'click .js-cancel-edit': 'onCancelEditDomain',
+      'click .js-delete': 'onDeleteDomain',
+      'click .js-save-domain': 'onSaveDomain'
+    },
+
+    serialize: function () {
+      return {
+        url: this.model.get('origins').split(',')[this.index]
+      };
+    },
+
+    onEditDomain: function () {
+
+      // show the editable field & save button
+      this.$('.js-url').addClass('hide');
+      this.$('.js-edit-domain').removeClass('hide');
+
+      // change the edit icon to a cancel icon
+      this.$('.js-edit').removeClass('js-edit fonticon-pencil')
+        .addClass('js-cancel-edit fonticon-cancel').attr("title", "Click to cancel");
+      this.$('.js-edit-domain input').select();
+    },
+
+    onCancelEditDomain: function () {
+      this.$('.js-url').removeClass('hide');
+      this.$('.js-edit-domain').addClass('hide');
+      this.$('.js-cancel-edit').removeClass('js-cancel-edit fonticon-cancel')
+        .addClass('js-edit fonticon-pencil').attr("title", "Click to edit");
+    },
+
+    onSaveDomain: function () {
+      var newDomain = this.$('.js-edit-domain input').val();
+
+      if (!CORS.validateCORSDomain(newDomain)) {
+        FauxtonAPI.addNotification({
+          msg: "Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).",
+          type: "error",
+          clear: true
+        });
+        return;
+      }
+
+      var domains = this.model.get('origins').split(',');
+      domains[this.index] = newDomain;
+      this.saveOrigins(domains);
+    },
+
+    // remove the domain from the list
+    onDeleteDomain: function () {
+
+      // remove the domain from the list. Anyone monitoring the object will hear that it's changed (e.g.
+      // the main table, which will know to re-render)
+      var domains = this.model.get('origins').split(',');
+      domains.splice(this.index, 1);
+
+      this.saveOrigins(domains);
+    },
+
+    saveOrigins: function (origins) {
+      var originDomains = origins.toString();
+      
+      var allowOrigins = new CORS.ConfigModel({
+        section: 'cors',
+        attribute: 'origins',
+        value: originDomains
+      });
+      
+      allowOrigins.save().then(function (response) {
+        var notification = FauxtonAPI.addNotification({
+          msg: 'Your origin domains have been updated.',
+          type: 'success',
+          clear: true
+        });
+      },
+      function (response, errorCode, errorMsg) {
+        var notification = FauxtonAPI.addNotification({
+          msg: 'Something went wrong.',
+          type: 'error',
+          clear: true
+        });
+      });
+    }
+  });
+  
+  CORS.Views = Views;
+
+  return CORS;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/settings.json.default
----------------------------------------------------------------------
diff --git a/settings.json.default b/settings.json.default
index 8e88c0e..1e6aa50 100644
--- a/settings.json.default
+++ b/settings.json.default
@@ -6,6 +6,7 @@
   { "name": "activetasks" },
   { "name": "config" },
   { "name": "replication" },
+  { "name": "cors" },
   { "name": "plugins" },
   { "name": "permissions" },
   { "name": "compaction" },


[2/2] fauxton commit: updated refs/heads/master to ddef3d5

Posted by ga...@apache.org.
CORS Support

Fixes COUCHDB-2483


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ddef3d54
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ddef3d54
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ddef3d54

Branch: refs/heads/master
Commit: ddef3d54ccbe08a9767977e0cf349e7bc652fb5f
Parents: a9636bd
Author: Garren Smith <ga...@gmail.com>
Authored: Tue Jan 20 10:02:34 2015 +0200
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Feb 2 09:31:14 2015 +0200

----------------------------------------------------------------------
 .gitignore                                      |   1 +
 app/addons/config/resources.js                  |   1 -
 app/addons/config/routes.js                     |  12 +-
 app/addons/cors/actions.js                      | 151 +++++++++
 app/addons/cors/actiontypes.js                  |  24 ++
 app/addons/cors/assets/less/cors.less           |  34 +-
 app/addons/cors/base.js                         |   2 +-
 app/addons/cors/components.react.jsx            | 328 +++++++++++++++++++
 app/addons/cors/resources.js                    |  56 +++-
 app/addons/cors/routes.js                       |  21 --
 app/addons/cors/stores.js                       | 162 +++++++++
 app/addons/cors/templates/cors.html             |  50 ---
 .../cors/templates/origin_domain_row.html       |  33 --
 .../cors/templates/origin_domain_table.html     |  17 -
 app/addons/cors/tests/actionsSpecs.js           | 153 +++++++++
 app/addons/cors/tests/componentsSpec.react.jsx  | 220 +++++++++++++
 app/addons/cors/tests/resourcesSpec.js          |  41 +++
 app/addons/cors/tests/storesSpec.js             | 104 ++++++
 app/addons/cors/views.js                        | 324 ++----------------
 app/templates/layouts/with_tabs_sidebar.html    |   2 +-
 assets/less/templates.less                      |   2 +-
 21 files changed, 1269 insertions(+), 469 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index db58376..7d5a71f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ app/addons/*
 !app/addons/databases
 !app/addons/documents
 !app/addons/styletests
+!app/addons/cors
 settings.json*
 !settings.json.default
 !assets/js/plugins/zeroclipboard/ZeroClipboard.swf

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/config/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js
index 895e4d8..b1b4080 100644
--- a/app/addons/config/resources.js
+++ b/app/addons/config/resources.js
@@ -45,7 +45,6 @@ function (app, FauxtonAPI) {
       } else {
         params.type = 'PUT';
       }
-
       return $.ajax(params);
     }
   });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/config/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 32dc4cc..85b8ff6 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -25,8 +25,9 @@ function(app, FauxtonAPI, Config, Views, CORS) {
 
     initialize: function () {
       this.configs = new Config.Collection();
-      this.cors = new CORS.config();
-      
+      this.cors = new CORS.Config();
+      this.httpd = new CORS.Httpd();
+
       this.sidebar = this.setView("#sidebar-content", new Views.Tabs({
         sidebarItems: [
           {
@@ -64,10 +65,13 @@ function(app, FauxtonAPI, Config, Views, CORS) {
       this.setView('#dashboard-content', new Views.Table({ collection: this.configs }));
       this.sidebar.setSelectedTab("main");
     },
-    
+
     configCORS: function() {
       this.removeView('#right-header');
-      this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSMain({ model: this.cors }));
+      this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSWrapper({
+        cors: this.cors,
+        httpd: this.httpd
+      }));
       this.sidebar.setSelectedTab("cors");
     },
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/actions.js b/app/addons/cors/actions.js
new file mode 100644
index 0000000..bb9e25b
--- /dev/null
+++ b/app/addons/cors/actions.js
@@ -0,0 +1,151 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'api',
+  'addons/cors/actiontypes',
+  'addons/cors/resources'
+  ], function (FauxtonAPI, ActionTypes, Resources) {
+    return {
+      editCors: function (options) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.EDIT_CORS,
+          options: options
+        });
+      },
+
+      toggleEnableCors: function () {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.TOGGLE_ENABLE_CORS
+        });
+      },
+
+      addOrigin: function (origin) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.CORS_ADD_ORIGIN,
+          origin: origin
+        });
+      },
+
+      originChange: function (isAllOrigins) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.CORS_IS_ALL_ORIGINS,
+          isAllOrigins: isAllOrigins
+        });
+      },
+
+      deleteOrigin: function (origin) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.CORS_DELETE_ORIGIN,
+          origin: origin
+        });
+      },
+
+      updateOrigin: function (updatedOrigin, originalOrigin) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.CORS_UPDATE_ORIGIN,
+          updatedOrigin: updatedOrigin,
+          originalOrigin: originalOrigin
+        });
+      },
+      methodChange: function (httpMethod) {
+        FauxtonAPI.dispatch({
+          type: ActionTypes.CORS_METHOD_CHANGE,
+          httpMethod: httpMethod
+        });
+      },
+
+      saveEnableCorsToHttpd: function (enableCors) {
+        var enableOption = new Resources.ConfigModel({
+          section: 'httpd',
+          attribute: 'enable_cors',
+          value: enableCors.toString()
+        });
+
+        return enableOption.save();
+      },
+
+      saveCorsOrigins: function (origins) {
+        var allowOrigins = new Resources.ConfigModel({
+          section: 'cors',
+          attribute: 'origins',
+          value: origins
+        });
+
+        return allowOrigins.save();
+      },
+
+      saveCorsCredentials: function () {
+        var allowCredentials = new Resources.ConfigModel({
+          section: 'cors',
+          attribute: 'credentials',
+          value: "true"
+        });
+
+        return allowCredentials.save();
+      },
+
+      saveCorsHeaders: function () {
+        var corsHeaders = new Resources.ConfigModel({
+          section: 'cors',
+          attribute: 'headers',
+          value: 'accept, authorization, content-type, origin, referer'
+        });
+
+        return corsHeaders.save();
+      },
+
+      saveCorsMethods: function () {
+        var corsMethods = new Resources.ConfigModel({
+          section: 'cors',
+          attribute: 'methods',
+          value: 'GET, PUT, POST, HEAD, DELETE'
+        });
+
+        return corsMethods.save();
+      },
+
+      sanitizeOrigins: function (origins) {
+        if(_.isEmpty(origins)) {
+          return '';
+        }
+
+        return origins.join(',');
+      },
+
+      saveCors: function (options) {
+        var promises = [];
+        promises.push(this.saveEnableCorsToHttpd(options.enableCors));
+
+        if(options.enableCors) {
+          promises.push(this.saveCorsOrigins(this.sanitizeOrigins(options.origins)));
+          promises.push(this.saveCorsCredentials());
+          promises.push(this.saveCorsHeaders());
+          promises.push(this.saveCorsMethods());
+        }
+
+        FauxtonAPI.when(promises).then(function () {
+          FauxtonAPI.addNotification({
+            msg: 'Cors settings updated',
+            type: 'success',
+            clear: true
+          });
+
+        }, function () {
+          FauxtonAPI.addNotification({
+            msg: 'Error! Could not save your CORS settings. Please try again.',
+            type: 'error',
+            clear: true
+          });
+        });
+      }
+    };
+  });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/actiontypes.js b/app/addons/cors/actiontypes.js
new file mode 100644
index 0000000..7e4916b
--- /dev/null
+++ b/app/addons/cors/actiontypes.js
@@ -0,0 +1,24 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([], function () {
+  return {
+    TOGGLE_ENABLE_CORS: 'TOGGLE_ENABLE_CORS',
+    EDIT_CORS: 'EDIT_CORS',
+    CORS_ADD_ORIGIN: 'CORS_ADD_ORIGIN',
+    CORS_IS_ALL_ORIGINS: 'CORS_IS_ALL_ORIGINS',
+    CORS_DELETE_ORIGIN: 'CORS_DELETE_ORIGIN',
+    CORS_UPDATE_ORIGIN: 'CORS_UPDATE_ORIGIN',
+    CORS_METHOD_CHANGE: 'CORS_METHOD_CHANGE',
+    CORS_SAVING: 'CORS_SAVING',
+    CORS_SAVED: 'CORS_SAVED'
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/assets/less/cors.less
----------------------------------------------------------------------
diff --git a/app/addons/cors/assets/less/cors.less b/app/addons/cors/assets/less/cors.less
index 4e06032..ce6dc7e 100644
--- a/app/addons/cors/assets/less/cors.less
+++ b/app/addons/cors/assets/less/cors.less
@@ -48,23 +48,19 @@
     border-bottom: 1px solid #ccc;
   }
   #origin-domains-container {
-    display: none;
-    
-    .new-origin-domain {
-      width: 100%;
-      border-radius: 6px;
+    button.add-domain {
+      height: 46px;
     }
   }
   #collapsing-container {
     border-top: 1px solid #fff;
     border-bottom: 1px solid #ccc;
     padding: 18px 30px;
-    display: none;
   }
   .origin-domains {
     margin: 0;
     padding: 18px 0px;
-    
+
     p {
       line-height: 36px;
     }
@@ -82,6 +78,14 @@
     margin-top: 0;
     border-top: 1px solid #fff;
   }
+
+  .input-append {
+      width: 90%;
+  }
+
+  input[type='text'] {
+    width: 100%;
+  }
 }
 
 #origin-domain-table {
@@ -96,26 +100,14 @@
   }
 
   .edit-domain-section {
-    position: relative;
-    margin-right: 20px;
-
-    .field-wrapper {
-      margin-right: 78px;
-    }
-    input {
-      width: 100%;
-      padding: 9px;
-      margin: 0 0 0 4px;
+    .btn {
+      height: 46px;
     }
   }
   .url-display {
     padding: 10px;
   }
 
-  .hide {
-    display: none;
-  }
-
   .update-domain-btn {
     position: absolute;
     right: -2px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/base.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/base.js b/app/addons/cors/base.js
index b5824e3..300b5b7 100644
--- a/app/addons/cors/base.js
+++ b/app/addons/cors/base.js
@@ -13,7 +13,7 @@
 define([
   "app",
   "api",
-  "addons/cors/routes"
+  "addons/cors/views"
 ],
 
 function (app, FauxtonAPI, CORS) {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/cors/components.react.jsx b/app/addons/cors/components.react.jsx
new file mode 100644
index 0000000..1de76c1
--- /dev/null
+++ b/app/addons/cors/components.react.jsx
@@ -0,0 +1,328 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  "api",
+  "react",
+  "addons/cors/stores",
+  "addons/cors/resources",
+  "addons/cors/actions"
+], function (FauxtonAPI, React, Stores, Resources, Actions) {
+  var corsStore = Stores.corsStore;
+
+  var validateOrigin = function (origin) {
+    if (!Resources.validateCORSDomain(origin)) {
+      FauxtonAPI.addNotification({
+        msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
+        type: 'error',
+        clear: true
+      });
+
+      return false;
+    }
+
+    return true;
+  };
+
+  var OriginRow = React.createClass({
+
+    getInitialState: function () {
+      return {
+        edit: false,
+        updatedOrigin: this.props.origin
+      };
+    },
+
+    editOrigin: function (event) {
+      event.preventDefault();
+      this.setState({edit: !this.state.edit});
+    },
+
+    updateOrigin: function (event) {
+      event.preventDefault();
+      if (!validateOrigin(this.state.updatedOrigin)) {
+        return;
+      }
+      this.props.updateOrigin(this.state.updatedOrigin, this.props.origin);
+      this.setState({edit: false});
+    },
+
+    deleteOrigin: function (event) {
+      event.preventDefault();
+      if (!window.confirm('Are you sure you want to delete ' + this.props.origin)) {
+        return;
+      }
+
+      this.props.deleteOrigin(this.props.origin);
+    },
+
+    onInputChange: function (event) {
+      this.setState({updatedOrigin: event.target.value});
+    },
+
+    createOriginDisplay: function () {
+      if (this.state.edit) {
+        return (
+          <div className="input-append edit-domain-section">
+            <input type="text" name="update_origin_domain" onChange={this.onInputChange} value={this.state.updatedOrigin} />
+            <button onClick={this.updateOrigin} className="btn btn-primary update-origin"> Update </button>
+          </div>
+        );
+      }
+
+      return <div className="js-url url-display">{this.props.origin}</div>;
+    },
+
+    render: function () {
+      var display = this.createOriginDisplay();
+      return (
+        <tr>
+          <td>
+            { display }
+          </td>
+          <td width="30">
+            <span className="fonticon-pencil" onClick={this.editOrigin} title="Click to edit"></span>
+          </td>
+          <td width="30">
+            <span className="fonticon-trash" onClick={this.deleteOrigin} title="Click to delete"></span>
+          </td>
+        </tr>
+      );
+    }
+
+  });
+
+  var OriginTable = React.createClass({
+
+    createRows: function () {
+      return _.map(this.props.origins, function (origin, i) {
+        return <OriginRow
+          updateOrigin={this.props.updateOrigin}
+          deleteOrigin={this.props.deleteOrigin}
+          key={i} origin={origin}
+        />;
+      }, this);
+    },
+
+    render: function () {
+      if (!this.props.isVisible || this.props.origins.length === 0) {
+        return null;
+      }
+
+      var origins = this.createRows();
+
+      return (
+        <table id="origin-domain-table" className="table table-striped">
+          <tbody>
+            {origins}
+          </tbody>
+        </table>
+      );
+    }
+
+  });
+
+  var OriginInput = React.createClass({
+    getInitialState: function () {
+      return {
+        origin: ''
+      };
+    },
+
+    onInputChange: function (e) {
+      this.setState({origin: e.target.value});
+    },
+
+    addOrigin: function (event) {
+      event.preventDefault();
+      if (!validateOrigin(this.state.origin)) {
+        return;
+      }
+
+      this.props.addOrigin(this.state.origin);
+      this.setState({origin: ''});
+    },
+
+    render: function () {
+      if (!this.props.isVisible) {
+        return null;
+      }
+
+      return (
+        <div id= "origin-domains-container">
+          <div className= "origin-domains">
+            <div className="input-append">
+              <input type="text" name="new_origin_domain" onChange={this.onInputChange} value={this.state.origin} placeholder="e.g., https://site.com"/>
+              <button onClick={this.addOrigin} className="btn btn-primary add-domain"> Add </button>
+            </div>
+          </div>
+        </div>
+      );
+    }
+
+  });
+
+  var Origins = React.createClass({
+
+    onOriginChange: function (event) {
+      this.props.originChange(event.target.value === 'all');
+    },
+
+    render: function () {
+
+      if (!this.props.corsEnabled) {
+        return null;
+      }
+
+      return (
+        <div>
+          <p><strong> Origin Domains </strong> </p>
+          <p>Databases will accept requests from these domains: </p>
+          <label className="radio">
+            <input type="radio" checked={this.props.isAllOrigins} value="all" onChange={this.onOriginChange} name="all-domains"/> All origin domains ( * )
+          </label>
+          <label className="radio">
+            <input type="radio" checked={!this.props.isAllOrigins} value="selected" onChange={this.onOriginChange} name="selected-domains"/> Restrict to specific origin domains
+          </label>
+        </div>
+      );
+    }
+  });
+
+  var CORSController = React.createClass({
+
+    getStoreState: function () {
+      return {
+        corsEnabled: corsStore.isEnabled(),
+        origins: corsStore.getOrigins(),
+        isAllOrigins: corsStore.isAllOrigins(),
+        configChanged: corsStore.hasConfigChanged(),
+        savingStatus: corsStore.getSavingStatus()
+      };
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    componentDidMount: function () {
+      corsStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function() {
+      corsStore.off('change', this.onChange);
+    },
+
+    componentDidUpdate: function () {
+      this.save();
+    },
+
+    onChange: function () {
+      this.setState(this.getStoreState());
+    },
+
+    enableCorsChange: function (event) {
+      if (this.state.corsEnabled && !_.isEmpty(this.state.origins) && !this.state.isAllOrigins) {
+        var result = window.confirm('Are you sure? Disabling CORS will overwrite your specific origin domains.');
+        if (!result) { return; }
+      }
+
+      Actions.toggleEnableCors();
+    },
+
+    save: function (event) {
+      Actions.saveCors({
+        enableCors: this.state.corsEnabled,
+        origins: this.state.origins
+      });
+    },
+
+    deleteOrigin: function (origin) {
+      Actions.deleteOrigin(origin);
+    },
+
+    originChange: function (isAllOrigins) {
+      if (isAllOrigins && !_.isEmpty(this.state.origins)) {
+        var result = window.confirm('Are you sure? Switching to all origin domains will overwrite your specific origin domains.');
+        if (!result) { return; }
+      }
+
+      Actions.originChange(isAllOrigins);
+    },
+
+    addOrigin: function (origin) {
+      Actions.addOrigin(origin);
+    },
+
+    updateOrigin: function (updatedOrigin, originalOrigin) {
+      Actions.updateOrigin(updatedOrigin, originalOrigin);
+    },
+
+    methodChange: function (httpMethod) {
+      Actions.methodChange(httpMethod);
+    },
+
+    getCorsNotice: function () {
+      var msg = FauxtonAPI.getExtensions('cors:notice');
+      if (_.isUndefined(msg)) {
+        return 'Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.';
+      }
+
+      return msg;
+    },
+
+    render: function () {
+      var isVisible = _.all([this.state.corsEnabled, !this.state.isAllOrigins]);
+      var className = this.state.corsEnabled ? 'collapsing-container' : '';
+
+      return (
+        <div className="cors-page">
+          <header id="cors-header">
+            <p> {this.getCorsNotice()}</p>
+          </header>
+
+          <form id="corsForm" onSubmit={this.save}>
+            <div className="cors-enable">
+              <label className="checkbox">
+                <input type="checkbox" checked={this.state.corsEnabled} onChange={this.enableCorsChange} /> Enable CORS
+              </label>
+            </div>
+            <div id={className}>
+              <Origins corsEnabled={this.state.corsEnabled} originChange={this.originChange} isAllOrigins={this.state.isAllOrigins}/>
+              <OriginTable updateOrigin={this.updateOrigin} deleteOrigin={this.deleteOrigin} isVisible={isVisible} origins={this.state.origins} />
+              <OriginInput addOrigin={this.addOrigin} isVisible={isVisible} />
+            </div>
+
+            <div className="form-actions">
+            </div>
+
+          </form>
+        </div>
+      );
+    }
+  });
+
+  return {
+    renderCORS: function (el) {
+      React.render(<CORSController />, el);
+    },
+
+    removeCORS: function (el) {
+      React.unmountComponentAtNode(el);
+    },
+    CORSController: CORSController,
+    OriginInput: OriginInput,
+    Origins: Origins,
+    OriginTable: OriginTable,
+    OriginRow: OriginRow
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/resources.js b/app/addons/cors/resources.js
index bd3809e..4cf4f10 100644
--- a/app/addons/cors/resources.js
+++ b/app/addons/cors/resources.js
@@ -1,39 +1,65 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
 // use this file except in compliance with the License. You may obtain a copy of
 // the License at
 //
 //   http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
 
 define([
-  "app",
-  "api"
+  'app',
+  'api'
 ],
 
-function (app, FauxtonAPI) {  
+function (app, FauxtonAPI) {
   var CORS = FauxtonAPI.addon();
 
 
-  CORS.config = FauxtonAPI.Model.extend({
+  CORS.Config = FauxtonAPI.Model.extend({
     url: function() {
-      return app.host+"/_config/cors";
+      return app.host + '/_config/cors';
+    },
+
+    getOrigins: function () {
+      var origins = this.get('origins');
+      if (_.isUndefined(origins)) {
+        return [];
+      }
+
+      return origins.split(',');
+    }
+  });
+
+  CORS.Httpd = FauxtonAPI.Model.extend({
+    url: function() {
+      return app.host + '/_config/httpd';
+    },
+
+    corsEnabled: function () {
+      var enabledCors = this.get('enable_cors');
+
+      if (_.isUndefined(enabledCors)) {
+        return false;
+      }
+
+      return enabledCors === 'true';
     }
+
   });
-  
+
   CORS.ConfigModel = Backbone.Model.extend({
-    documentation: "cors",
-    
+    documentation: 'cors',
+
     url: function () {
-      return app.host + '/_config/' + encodeURIComponent(this.get("section")) + '/' + encodeURIComponent(this.get("attribute"));
+      return app.host + '/_config/' + encodeURIComponent(this.get('section')) + '/' + encodeURIComponent(this.get('attribute'));
     },
-    
+
     isNew: function () { return false; },
-    
+
     sync: function (method, model, options) {
 
       var params = {
@@ -51,9 +77,9 @@ function (app, FauxtonAPI) {
 
       return $.ajax(params);
     }
-  
+
   });
-  
+
     // simple helper function to validate the user entered a valid domain starting with http(s) and
   // not including any subfolder
   CORS.validateCORSDomain = function (str) {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/routes.js b/app/addons/cors/routes.js
deleted file mode 100644
index 017211b..0000000
--- a/app/addons/cors/routes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-define([
-  'app',
-  'api',
-  'addons/cors/views'
-],
-
-function (app, FauxtonAPI, CORS) {
-  return CORS;
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/stores.js b/app/addons/cors/stores.js
new file mode 100644
index 0000000..dc70cbd
--- /dev/null
+++ b/app/addons/cors/stores.js
@@ -0,0 +1,162 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  "api",
+  "addons/cors/actiontypes"
+], function (FauxtonAPI, ActionTypes) {
+
+  var CorsStore = FauxtonAPI.Store.extend({
+
+    editCors: function (options) {
+      this._isEnabled = options.isEnabled;
+      this._origins = options.origins;
+      this._configChanged = false;
+      this.savingDone();
+    },
+
+    saving: function () {
+        this._savingStatus = 'Saving';
+    },
+
+    savingDone: function () {
+      this._savingStatus = 'Save';
+    },
+
+    getSavingStatus: function () {
+      return this._savingStatus;
+    },
+
+    hasConfigChanged: function () {
+      return this._configChanged;
+    },
+
+    setConfigChanged: function () {
+      this._configChanged = true;
+    },
+
+    setConfigSaved: function () {
+      this._configChanged = false;
+    },
+
+    isEnabled: function () {
+      return this._isEnabled;
+    },
+
+    addOrigin: function (origin) {
+      this._origins.push(origin);
+    },
+
+    deleteOrigin: function (origin) {
+      var index = _.indexOf(this._origins, origin);
+
+      if (index === -1) { return; }
+
+      this._origins.splice(index, 1);
+    },
+
+    originChange: function (isAllOrigins) {
+      if (isAllOrigins) {
+        this._origins = ['*'];
+        return;
+      }
+
+      this._origins = [];
+    },
+
+    getOrigins: function () {
+      return this._origins;
+    },
+
+    isAllOrigins: function () {
+      var origins = this.getOrigins();
+      if (_.include(origins, '*')) {
+        return true;
+      }
+
+      return false;
+    },
+
+    toggleEnableCors: function () {
+      this._isEnabled = !this._isEnabled;
+    },
+
+    updateOrigin: function (updatedOrigin, originalOrigin) {
+      this.deleteOrigin(originalOrigin);
+      this.addOrigin(updatedOrigin);
+    },
+
+    dispatch: function (action) {
+
+      switch (action.type) {
+        case ActionTypes.EDIT_CORS:
+          this.editCors(action.options);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.TOGGLE_ENABLE_CORS:
+          this.toggleEnableCors();
+          this.setConfigChanged();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_ADD_ORIGIN:
+          this.addOrigin(action.origin);
+          this.setConfigChanged();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_IS_ALL_ORIGINS:
+          this.originChange(action.isAllOrigins);
+          this.setConfigChanged();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_DELETE_ORIGIN:
+          this.deleteOrigin(action.origin);
+          this.setConfigChanged();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_UPDATE_ORIGIN:
+          this.updateOrigin(action.updatedOrigin, action.originalOrigin);
+          this.setConfigChanged();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_SAVING:
+          this.saving();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.CORS_SAVED:
+          this.setConfigSaved();
+          this.savingDone();
+          this.triggerChange();
+        break;
+
+        default:
+          return;
+      }
+    }
+
+  });
+
+
+  var corsStore = new CorsStore();
+
+  corsStore.dispatchToken = FauxtonAPI.dispatcher.register(corsStore.dispatch.bind(corsStore));
+
+  return {
+    corsStore: corsStore
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/cors.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/cors.html b/app/addons/cors/templates/cors.html
deleted file mode 100644
index 8db6a60..0000000
--- a/app/addons/cors/templates/cors.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
-Licensed under the Apache License, Version 2.0 (the "License"); you may not
-use this file except in compliance with the License. You may obtain a copy of
-the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-License for the specific language governing permissions and limitations under
-the License.
--->
-
-<header id="cors-header">
-  <p>Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.</p>
-</header>
-
-<form id="corsForm">
-
-  <div class="cors-enable">
-    <label class="checkbox">
-      <input type="checkbox" class="js-enable-cors" name="enable_cors" <% if  (typeof enableCors !== 'undefined' && enableCors === 'true') { %> checked="checked" <% } %>> Enable CORS
-    </label>
-  </div>
-  
-  <div id="collapsing-container">
-    <p><strong>Origin Domains</strong></p>
-    
-    <p>Databases will accept requests from these domains:</p>
-    
-    <label class="checkbox"><input type="checkbox" class="js-all-origin-domains" name="all_origin_domains"> All origin domains ( * )</label></li>
-        
-    <label class="checkbox"><input type="checkbox" class="js-restrict-origin-domains" name="restrict_origin_domains"> Restrict to specific origin domains</label>
-      
-    <div id="origin-domains-container">
-      <div class="origin-domains"></div>
-      <input type="text" class="new-origin-domain" name="new_origin_domain" value="" placeholder="e.g., https://site.com" />
-    </div>
-    
-    </div>
-    
-  </div>
-
-  <div class="form-actions">
-    <input class="btn btn-success" type="submit" value="Save" />
-  </div>
-
-</form>
-

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/origin_domain_row.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_row.html b/app/addons/cors/templates/origin_domain_row.html
deleted file mode 100644
index 19f6954..0000000
--- a/app/addons/cors/templates/origin_domain_row.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-Licensed under the Apache License, Version 2.0 (the "License"); you may not
-use this file except in compliance with the License. You may obtain a copy of
-the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-License for the specific language governing permissions and limitations under
-the License.
--->
-
-<td>
-  <div class="js-url url-display"><%-url%></div>
-
-  <div class="js-edit-domain edit-domain-section hide">
-    <div class="field-wrapper">
-      <input type="text" value="<%-url%>" />
-    </div>
-    <a class="js-save-domain btn update-domain-btn">
-      <i class="fonticon-save"></i>
-      Update
-    </a>
-  </div>
-</td>
-<td width="30">
-  <span class="js-edit fonticon-pencil" title="Click to edit"></span>
-</td>
-<td width="30">
-  <span class="js-delete fonticon-trash" title="Click to delete"></span>
-</td>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/origin_domain_table.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_table.html b/app/addons/cors/templates/origin_domain_table.html
deleted file mode 100644
index 7192bea..0000000
--- a/app/addons/cors/templates/origin_domain_table.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!--
-Licensed under the Apache License, Version 2.0 (the "License"); you may not
-use this file except in compliance with the License. You may obtain a copy of
-the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-License for the specific language governing permissions and limitations under
-the License.
--->
-
-<table id="origin-domain-table" class="table table-striped">
-  <tbody></tbody>
-</table>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/actionsSpecs.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/actionsSpecs.js b/app/addons/cors/tests/actionsSpecs.js
new file mode 100644
index 0000000..9b74634
--- /dev/null
+++ b/app/addons/cors/tests/actionsSpecs.js
@@ -0,0 +1,153 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'app',
+  'testUtils',
+  'api',
+  'addons/cors/actions',
+], function (app, testUtils, FauxtonAPI, Actions) {
+  var assert = testUtils.assert;
+
+  describe('CORS actions', function () {
+
+    describe('save', function () {
+
+      afterEach(function () {
+        Actions.saveCorsOrigins.restore && Actions.saveCorsOrigins.restore();
+      });
+
+      it('should save cors enabled to httpd', function () {
+        var spy = sinon.spy(Actions, 'saveEnableCorsToHttpd');
+
+        Actions.saveCors({
+          enableCors: false
+        });
+
+        assert.ok(spy.calledWith(false));
+      });
+
+      it('does not save cors origins if cors not enabled', function () {
+        var spy = sinon.spy(Actions, 'saveCorsOrigins');
+
+        Actions.saveCors({
+          enableCors: false,
+          origins: ['*']
+        });
+
+        assert.notOk(spy.calledOnce);
+      });
+
+      it('saves cors origins', function () {
+        var spy = sinon.spy(Actions, 'saveCorsOrigins');
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['*']
+        });
+
+        assert.ok(spy.calledWith('*'));
+      });
+
+      it('saves cors allow credentials', function () {
+        var spy = sinon.spy(Actions, 'saveCorsCredentials');
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['https://testdomain.com']
+        });
+
+        assert.ok(spy.calledOnce);
+      });
+
+      it('saves cors headers', function () {
+        var spy = sinon.spy(Actions, 'saveCorsHeaders');
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['https://testdomain.com']
+        });
+
+        assert.ok(spy.calledOnce);
+      });
+
+      it('saves cors methods', function () {
+        var spy = sinon.spy(Actions, 'saveCorsMethods');
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['https://testdomain.com']
+        });
+
+        assert.ok(spy.calledOnce);
+
+      });
+
+      it('shows notification on successful save', function () {
+        var stub = sinon.stub(FauxtonAPI, 'when');
+        var spy = sinon.spy(FauxtonAPI, 'addNotification');
+        var promise = FauxtonAPI.Deferred();
+        promise.resolve();
+        stub.returns(promise);
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['https://testdomain.com']
+        });
+
+        assert.ok(spy.calledOnce);
+        FauxtonAPI.when.restore();
+        FauxtonAPI.addNotification.restore();
+      });
+
+      it('dispatches CORS_SAVED', function () {
+        var stub = sinon.stub(FauxtonAPI, 'when');
+        var called = false;
+        var promise = FauxtonAPI.Deferred();
+        promise.resolve();
+        stub.returns(promise);
+
+        FauxtonAPI.dispatcher.register(function (actions) {
+
+          if (actions.type === 'CORS_SAVED') {
+            called = true;
+          }
+
+        });
+
+        Actions.saveCors({
+          enableCors: true,
+          origins: ['https://testdomain.com']
+        });
+
+        assert.ok(called);
+        FauxtonAPI.when.restore();
+      });
+
+    });
+
+    describe('Sanitize origins', function () {
+
+      it('joins array into string', function () {
+        var origins = ['https://hello.com', 'https://hello2.com'];
+
+        assert.deepEqual(Actions.sanitizeOrigins(origins), origins.join(','));
+      });
+
+      it('returns empty string for no origins', function () {
+        var origins = [];
+
+        assert.deepEqual(Actions.sanitizeOrigins(origins), '');
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/componentsSpec.react.jsx b/app/addons/cors/tests/componentsSpec.react.jsx
new file mode 100644
index 0000000..5880257
--- /dev/null
+++ b/app/addons/cors/tests/componentsSpec.react.jsx
@@ -0,0 +1,220 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'api',
+  'addons/cors/components.react',
+  'addons/cors/actions',
+  'addons/cors/resources',
+  'addons/cors/stores',
+  'testUtils',
+  "react"
+], function (FauxtonAPI, Views, Actions, Resources, Stores, utils, React) {
+
+  FauxtonAPI.router = new FauxtonAPI.Router([]);
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+  var corsStore = Stores.corsStore;
+
+  describe('CORS Components', function () {
+
+
+    describe('CorsController', function () {
+      var container, corsEl, saveStub;
+
+      beforeEach(function () {
+        container = document.createElement('div');
+        corsStore._origins = ['http://hello.com'];
+        corsStore._isEnabled = true;
+        corsStore._configChanged = true;
+        corsEl = TestUtils.renderIntoDocument(<Views.CORSController />, container);
+        //stub this out so it doesn't keep trying to save cors and crash phantomjs
+        saveStub = sinon.stub(corsEl, 'save');
+      });
+
+      afterEach(function () {
+        corsEl.save.restore();
+        React.unmountComponentAtNode(container);
+        window.confirm.restore && window.confirm.restore();
+      });
+
+      it('confirms user change from restricted origin to disabled cors', function () {
+        var spy = sinon.stub(window, 'confirm');
+        spy.returns(false);
+        corsEl.state.isAllOrigins = false;
+        corsEl.state.corsEnabled = true;
+        corsEl.enableCorsChange();
+        assert.ok(spy.calledOnce);
+      });
+
+      it('does not confirm for selected origins are emtpy for disabled cors change', function () {
+        var spy = sinon.stub(window, 'confirm');
+        spy.returns(false);
+        corsEl.state.corsEnabled = true;
+        corsEl.state.isAllOrigins = false;
+        corsEl.state.origins = [];
+        corsEl.enableCorsChange();
+        assert.notOk(spy.calledOnce);
+      });
+
+      it('confirms user change when moving from selected origins to all origins', function () {
+        var spy = sinon.stub(window, 'confirm');
+        spy.returns(false);
+        corsEl.state.corsEnabled = true;
+        corsEl.state.isAllOrigins = false;
+        corsEl.originChange(true);
+        assert.ok(spy.calledOnce);
+      });
+
+      it('does not confirm all origins change if selected origins are emtpy', function () {
+        var spy = sinon.stub(window, 'confirm');
+        spy.returns(false);
+        corsEl.state.corsEnabled = true;
+        corsEl.state.isAllOrigins = false;
+        corsEl.state.origins = [];
+        corsEl.originChange(true);
+        assert.notOk(spy.calledOnce);
+      });
+    });
+
+    describe('OriginInput', function () {
+      var container, inputEl, addOrigin;
+      var newOrigin = 'http://new-site.com';
+
+      beforeEach(function () {
+        addOrigin = sinon.spy();
+        container = document.createElement('div');
+        inputEl = TestUtils.renderIntoDocument(<Views.OriginInput isVisible={true} addOrigin={addOrigin}/>, container);
+      });
+
+      afterEach(function () {
+        Resources.validateCORSDomain.restore && Resources.validateCORSDomain.restore();
+        React.unmountComponentAtNode(container);
+        FauxtonAPI.addNotification.restore && FauxtonAPI.addNotification.restore();
+      });
+
+      it('calls validates each domain', function () {
+        var spy = sinon.spy(Resources, 'validateCORSDomain');
+        TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: newOrigin}});
+        TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+        assert.ok(spy.calledWith(newOrigin));
+      });
+
+      it('calls addOrogin AddOrigin on add click with valid domain', function () {
+        TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: newOrigin}});
+        TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+        assert.ok(addOrigin.calledWith(newOrigin));
+      });
+
+      it('shows notification if origin is not valid', function () {
+        var spy = sinon.spy(FauxtonAPI, 'addNotification');
+        TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: 'badOrigin'}});
+        TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+        assert.ok(spy.calledOnce);
+      });
+    });
+
+    describe('Origins', function () {
+      var container, originEl, changeOrigin;
+
+      beforeEach(function () {
+        changeOrigin = sinon.spy();
+        container = document.createElement('div');
+        originEl = TestUtils.renderIntoDocument(<Views.Origins corsEnabled={true} isAllOrigins={false} originChange={changeOrigin}/>, container);
+      });
+
+      afterEach(function () {
+        React.unmountComponentAtNode(container);
+      });
+
+      it('calls change Origin on all origins selected', function () {
+        TestUtils.Simulate.change($(originEl.getDOMNode()).find('input[value="all"]')[0]);
+        assert.ok(changeOrigin.calledWith(true));
+      });
+
+      it('calls change Origin on selected origins selected', function () {
+        TestUtils.Simulate.change($(originEl.getDOMNode()).find('input[value="selected"]')[0]);
+        assert.ok(changeOrigin.calledWith(false));
+      });
+    });
+
+    describe('OriginRow', function () {
+      var container, originTableEl, origin, deleteOrigin, updateOrigin;
+
+      beforeEach(function () {
+        deleteOrigin = sinon.spy();
+        updateOrigin = sinon.spy();
+        container = document.createElement('div');
+        origin = 'https://hello.com';
+        //because OriginRow is inside a table have to render the whole table to test
+        originTableEl = TestUtils.renderIntoDocument(<Views.OriginTable updateOrigin={updateOrigin} deleteOrigin={deleteOrigin} isVisible={true} origins={[origin]}/>, container);
+      });
+
+      afterEach(function () {
+        window.confirm.restore && window.confirm.restore();
+        Actions.deleteOrigin.restore && Actions.deleteOrigin.restore();
+        React.unmountComponentAtNode(container);
+      });
+
+      it('should confirm on delete', function () {
+        var spy = sinon.spy(window, 'confirm');
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+        assert.ok(spy.calledOnce);
+      });
+
+      it('should deleteOrigin on confirm true', function () {
+        var stub = sinon.stub(window, 'confirm');
+        stub.returns(true);
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+        assert.ok(deleteOrigin.calledWith(origin));
+      });
+
+      it('should not deleteOrigin on confirm false', function () {
+        var stub = sinon.stub(window, 'confirm');
+        stub.returns(false);
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+        assert.notOk(deleteOrigin.calledOnce);
+      });
+
+      it('should change origin to input on edit click', function () {
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+        assert.ok($(originTableEl.getDOMNode()).find('input').length === 1);
+      });
+
+      it('should update origin on update clicked', function () {
+        var updatedOrigin = 'https://updated-origin.com';
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+        TestUtils.Simulate.change($(originTableEl.getDOMNode()).find('input')[0], {
+          target: {
+            value: updatedOrigin
+          }
+        });
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.btn')[0]);
+        assert.ok(updateOrigin.calledWith(updatedOrigin));
+      });
+
+      it('should not update origin on update clicked with bad origin', function () {
+        var updatedOrigin = 'updated-origin';
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+        TestUtils.Simulate.change($(originTableEl.getDOMNode()).find('input')[0], {
+          target: {
+            value: updatedOrigin
+          }
+        });
+        TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.btn')[0]);
+        assert.notOk(updateOrigin.calledOnce);
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/resourcesSpec.js b/app/addons/cors/tests/resourcesSpec.js
new file mode 100644
index 0000000..2d2c8d0
--- /dev/null
+++ b/app/addons/cors/tests/resourcesSpec.js
@@ -0,0 +1,41 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'app',
+  'testUtils',
+  'addons/cors/resources',
+], function (app, testUtils, CORS) {
+  var assert = testUtils.assert;
+
+  describe('Cors Config Model', function () {
+    var cors;
+
+    beforeEach(function () {
+      cors = new CORS.Config({});
+    });
+
+    it('Splits up origins into array', function () {
+      var origins = ['http://hello.com', 'http://another.co.a'];
+      cors.set({origins: origins.join(',')});
+
+      assert.deepEqual(cors.getOrigins(), origins);
+    });
+
+    it('returns empty array for undefined', function () {
+
+      assert.deepEqual(cors.getOrigins(), []);
+    });
+
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/storesSpec.js b/app/addons/cors/tests/storesSpec.js
new file mode 100644
index 0000000..48a1897
--- /dev/null
+++ b/app/addons/cors/tests/storesSpec.js
@@ -0,0 +1,104 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'app',
+  'testUtils',
+  'api',
+  'addons/cors/stores',
+], function (app, testUtils, FauxtonAPI, Stores) {
+  var assert = testUtils.assert;
+  var store = Stores.corsStore;
+
+  describe('CORS store', function () {
+
+    describe('isAllOrigins', function () {
+
+      it('returns true for all origins', function () {
+        store._origins = ['*'];
+
+        assert.ok(store.isAllOrigins());
+      });
+
+      it('returns false for specific origins', function () {
+        store._origins = ['https://hello.com', 'http://another.com'];
+        assert.notOk(store.isAllOrigins());
+      });
+
+      it('returns false for empty array', function () {
+        store._origins = [];
+        assert.notOk(store.isAllOrigins());
+      });
+    });
+
+    describe('addOrigin', function () {
+
+      it('adds Origin to list', function () {
+        var origin = 'http://hello.com';
+        store._origins = [];
+        store.addOrigin(origin);
+
+        assert.ok(_.include(store.getOrigins(), origin));
+      });
+
+    });
+
+    describe('originChange', function () {
+
+      it('sets origins to * for true', function () {
+        store.originChange(true);
+
+        assert.deepEqual(store.getOrigins(), ['*']);
+      });
+
+      it('sets origins to [] for true', function () {
+        store.originChange(false);
+
+        assert.deepEqual(store.getOrigins(), []);
+      });
+
+    });
+
+    describe('deleteOrigin', function () {
+
+      it('removes origin', function () {
+        store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+        store.deleteOrigin('http://hello.com');
+
+        assert.deepEqual(store.getOrigins(), ['http://first.com', 'http://second.com']);
+
+      });
+
+    });
+
+    describe('update origin', function () {
+
+      it('removes old origin', function () {
+        store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+        store.updateOrigin('http://hello123.com', 'http://hello.com');
+
+        assert.notOk(_.include(store.getOrigins(), 'http://hello.com'));
+
+      });
+
+      it('adds new origin', function () {
+        store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+        store.updateOrigin('http://hello123.com', 'http://hello.com');
+
+        assert.ok(_.include(store.getOrigins(), 'http://hello123.com'));
+
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/views.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/views.js b/app/addons/cors/views.js
index c390d5a..341b427 100644
--- a/app/addons/cors/views.js
+++ b/app/addons/cors/views.js
@@ -13,324 +13,40 @@
 define([
   "app",
   "api",
-  "addons/cors/resources"
+  "addons/cors/resources",
+  "addons/cors/components.react",
+  "addons/cors/actions"
 ],
 
 
-function (app, FauxtonAPI, CORS) {
+function (app, FauxtonAPI, CORS, Components, Actions) {
   var Views= {};
-  
-  Views.CORSMain = FauxtonAPI.View.extend({
-    className: 'cors-page',
-    template: 'addons/cors/templates/cors',
-    events: {
-      'submit form#corsForm': 'submit',
-      'click .js-enable-cors': 'corsClick',
-      'click .js-restrict-origin-domains': 'restrictOrigins',
-      'click .js-all-origin-domains': 'allOrigins'
-    },
-    
-    initialize: function () {
-      this.originDomainTable = this.setView('.origin-domains', new Views.OriginDomainTable({
-        model: this.model
-      }));
-    },
-    
-    serialize: function () {
-      return {
-        enableCors: this.model.get('credentials')    
-      };
-    },
-    
-    establish: function(){
-      return [this.model.fetch()];
-    },
-    
-    afterRender: function () {        
-      var corsEnabled = this.$('.js-enable-cors').is(':checked');
-      this.$('#collapsing-container').toggle(corsEnabled);
-      this.setupOrigins();
-    },
-    
-    corsClick: function (e) {
-      var isChecked = this.$(e.target).prop('checked');
-      this.$('#collapsing-container').toggle(isChecked);
-      this.setupOrigins();
-    },
-    
-    
-    setupOrigins: function() {
-      var storedOrigins = this.model.get('origins');
-      if (storedOrigins && storedOrigins != '*') {
-        this.restrictOrigins();
-      } else {
-        this.allOrigins();
-      }
-    },
-    
-    allOrigins: function() {
-      this.$('.js-all-origin-domains').prop('checked', true);
-      this.$('.js-restrict-origin-domains').prop('checked', false);
-      this.$('#origin-domains-container').hide();
-    },
 
-    restrictOrigins: function() {
-      this.$('.js-restrict-origin-domains').prop('checked', true);
-      this.$('.js-all-origin-domains').prop('checked', false);
-      this.$('#origin-domains-container').show();
+  Views.CORSWrapper = FauxtonAPI.View.extend({
+    className: 'list',
+    initialize: function (options) {
+      this.cors = options.cors;
+      this.httpd = options.httpd;
     },
-    
-    formToJSON: function(formSelector){
-      var formObject = $(formSelector).serializeArray(),
-        formJSON={};
-      _.map(formObject, function(field){
-        formJSON[field.name]=field.value;
-      });
-      return formJSON;
-    },
-    
-    submit: function(e){
-      e.preventDefault();      
-      var data = this.formToJSON(e.currentTarget);
-
-      if (data.enable_cors === 'on') {
 
-        // CORS checked, save data  
-        if (data.restrict_origin_domains === 'on') {
-          var storedOrigins = this.model.get('origins').split(',');
-          var newDomain = $.trim(data.new_origin_domain);
-	
-          // if a new domain has been entered, check it's valid
-          if (!_.isEmpty(newDomain) && !CORS.validateCORSDomain(newDomain)) {
-            FauxtonAPI.addNotification({
-              msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
-              type: 'error',
-              clear: true
-            });
-            return;
-          }
-          
-          // check that the user has entered at least one new origin domain
-          if (storedOrigins && storedOrigins.length > 0 && storedOrigins !== '*') {
-            this.originData = storedOrigins.concat(newDomain).toString();            
-          } else {
-            if (_.isEmpty(newDomain)) {
-              FauxtonAPI.addNotification({
-                msg: 'Please enter a new origin domain.',
-                type: 'error',
-                clear: true
-              });
-              this.$('.new-origin-domain').focus();
-              return;
-            }
-            this.originData = data.new_origin_domain;
-          }
-          
-        } else {
-          this.originData = "*";
-        }
-        
-          
-        var enableOption = new CORS.ConfigModel({
-          section: 'httpd',
-          attribute: 'enable_cors',
-          value: 'true'
-        });
-        
-        var enableCreds = new CORS.ConfigModel({
-          section: 'cors',
-          attribute: 'credentials',
-          value: 'true'
-        });
-        
-        var allowOrigins = new CORS.ConfigModel({
-          section: 'cors',
-          attribute: 'origins',
-          value: this.originData
-        });
-
-        enableOption.save().then(function (response) {
-          var notification = FauxtonAPI.addNotification({
-            msg: 'Your settings have been saved.',
-            type: 'success',
-            clear: true
-          });
-        },
-        function (response, errorCode, errorMsg) {
-          var notification = FauxtonAPI.addNotification({
-            msg: 'Sorry! There was an error. Code ' + errorCode  + '.',
-            type: 'error',
-            clear: true
-          });
-        });
-        
-        enableCreds.save();
-        allowOrigins.save();
-        this.$('.new-origin-domain').val('');
-      
-      } else {
-          
-        // Disable CORS
-        var disableOption = new CORS.ConfigModel({
-          section: 'httpd',
-          attribute: 'enable_cors',
-          value: 'false'
-        });
-        
-        var disableCreds = new CORS.ConfigModel({
-          section: 'cors',
-          attribute: 'credentials',
-          value: 'false'
-        });
-        
-        var disableOrigins = new CORS.ConfigModel({
-          section: 'cors',
-          attribute: 'origins',
-          value: ''
-        });
-        
-        disableOption.save().then(function (response) {
-          var notification = FauxtonAPI.addNotification({
-            msg: 'Your settings have been saved.',
-            type: 'success',
-            clear: true
-          });
-        },
-        function (response, errorCode, errorMsg) {
-          var notification = FauxtonAPI.addNotification({
-            msg: 'Sorry! There was an error. Code ' + errorCode  + '.',
-            type: 'error',
-            clear: true
-          });
-        });
-          
-        disableCreds.save();
-        disableOrigins.save();
-      }
-    }
-  });
-  
-  Views.OriginDomainTable = FauxtonAPI.View.extend({
-    template: 'addons/cors/templates/origin_domain_table',
-
-    initialize: function () {
-      // listen for any server-side changes to the object (i.e. saves/deletes). Only then, re-render the table
-      this.listenTo(this.model, 'sync', this.render);
+    establish: function () {
+      return [this.cors.fetch(), this.httpd.fetch()];
     },
 
-    beforeRender: function () {
-      var origins = this.model.get('origins');
-
-      // if the stored origins are set to '*' or nothing's defined, show nothing
-      if (_.isEmpty(origins) || origins === '*') {
-        return;
-      }
-      this.showRows();
+    afterRender: function () {
+      Actions.editCors({
+        origins: this.cors.getOrigins(),
+        isEnabled: this.httpd.corsEnabled()
+      });
+      Components.renderCORS(this.el);
     },
 
-    showRows: function () {
-        var originsArray = this.model.get('origins').split(',');
-      _.each(originsArray, function (url, index) {
-        this.insertView('#origin-domain-table tbody', new Views.OriginDomainRow({
-          model: this.model,
-          index: index
-        }));
-      }, this);
+    cleanup: function () {
+      Components.removeCORS(this.el);
     }
-  });
-
-
-  // this gets passed the entire model so it can manipulate it directly (add/update the row)
-  Views.OriginDomainRow = FauxtonAPI.View.extend({
-    template: 'addons/cors/templates/origin_domain_row',
-    tagName: 'tr',
-
-    events: {
-      'click .js-edit': 'onEditDomain',
-      'click .js-cancel-edit': 'onCancelEditDomain',
-      'click .js-delete': 'onDeleteDomain',
-      'click .js-save-domain': 'onSaveDomain'
-    },
-
-    serialize: function () {
-      return {
-        url: this.model.get('origins').split(',')[this.index]
-      };
-    },
-
-    onEditDomain: function () {
-
-      // show the editable field & save button
-      this.$('.js-url').addClass('hide');
-      this.$('.js-edit-domain').removeClass('hide');
-
-      // change the edit icon to a cancel icon
-      this.$('.js-edit').removeClass('js-edit fonticon-pencil')
-        .addClass('js-cancel-edit fonticon-cancel').attr("title", "Click to cancel");
-      this.$('.js-edit-domain input').select();
-    },
-
-    onCancelEditDomain: function () {
-      this.$('.js-url').removeClass('hide');
-      this.$('.js-edit-domain').addClass('hide');
-      this.$('.js-cancel-edit').removeClass('js-cancel-edit fonticon-cancel')
-        .addClass('js-edit fonticon-pencil').attr("title", "Click to edit");
-    },
-
-    onSaveDomain: function () {
-      var newDomain = this.$('.js-edit-domain input').val();
-
-      if (!CORS.validateCORSDomain(newDomain)) {
-        FauxtonAPI.addNotification({
-          msg: "Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).",
-          type: "error",
-          clear: true
-        });
-        return;
-      }
 
-      var domains = this.model.get('origins').split(',');
-      domains[this.index] = newDomain;
-      this.saveOrigins(domains);
-    },
-
-    // remove the domain from the list
-    onDeleteDomain: function () {
-
-      // remove the domain from the list. Anyone monitoring the object will hear that it's changed (e.g.
-      // the main table, which will know to re-render)
-      var domains = this.model.get('origins').split(',');
-      domains.splice(this.index, 1);
-
-      this.saveOrigins(domains);
-    },
-
-    saveOrigins: function (origins) {
-      var originDomains = origins.toString();
-      
-      var allowOrigins = new CORS.ConfigModel({
-        section: 'cors',
-        attribute: 'origins',
-        value: originDomains
-      });
-      
-      allowOrigins.save().then(function (response) {
-        var notification = FauxtonAPI.addNotification({
-          msg: 'Your origin domains have been updated.',
-          type: 'success',
-          clear: true
-        });
-      },
-      function (response, errorCode, errorMsg) {
-        var notification = FauxtonAPI.addNotification({
-          msg: 'Something went wrong.',
-          type: 'error',
-          clear: true
-        });
-      });
-    }
   });
-  
+
   CORS.Views = Views;
 
   return CORS;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/templates/layouts/with_tabs_sidebar.html
----------------------------------------------------------------------
diff --git a/app/templates/layouts/with_tabs_sidebar.html b/app/templates/layouts/with_tabs_sidebar.html
index e6acfa3..f58184d 100644
--- a/app/templates/layouts/with_tabs_sidebar.html
+++ b/app/templates/layouts/with_tabs_sidebar.html
@@ -26,7 +26,7 @@ the License.
 
     <aside id="sidebar-content" class="scrollable"></aside>
 
-    <section id="dashboard-content" class="list">
+    <section id="dashboard-content" class="list scrollable">
       <div class="scrollable">
         <div class="inner">
           <div id="dashboard-upper-content"></div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/assets/less/templates.less
----------------------------------------------------------------------
diff --git a/assets/less/templates.less b/assets/less/templates.less
index c0b6515..1e017ef 100644
--- a/assets/less/templates.less
+++ b/assets/less/templates.less
@@ -417,7 +417,7 @@ with_tabs_sidebar.html
     bottom: 0px;
     top: @collapsedNavWidth;
     position: fixed;
-    overflow: hidden;
+    overflow-x: hidden;
     left: @sidebarWidth+@navWidth;
     right: 0;
     .box-sizing(border-box);