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 2017/06/07 13:00:30 UTC

[couchdb-fauxton] 01/02: Clean up Auth section

This is an automated email from the ASF dual-hosted git repository.

garren pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git

commit 059964bf54fbc5b8e145f00ab5f022626a13fa92
Author: Tim Pinington <ti...@gmail.com>
AuthorDate: Wed May 10 16:48:48 2017 +0200

    Clean up Auth section
    
    Remove backbone and clean up auth
---
 app/addons/auth/__tests__/base.test.js             |  83 -----
 app/addons/auth/actions.js                         | 223 ++++++------
 app/addons/auth/actiontypes.js                     |   2 +-
 app/addons/auth/assets/less/auth.less              |   2 +-
 app/addons/auth/base.js                            | 133 +++----
 app/addons/auth/components.js                      | 384 ---------------------
 app/addons/auth/components/changepasswordform.js   |  92 +++++
 app/addons/auth/components/createadminform.js      | 119 +++++++
 app/addons/auth/components/createadminsidebar.js   |  61 ++++
 app/addons/auth/components/index.js                |  25 ++
 app/addons/auth/components/loginform.js            | 110 ++++++
 app/addons/auth/components/passwordmodal.js        | 107 ++++++
 app/addons/auth/layout.js                          |   8 +-
 app/addons/auth/resources.js                       | 208 -----------
 app/addons/auth/routes.js                          | 119 -------
 app/addons/auth/routes/auth.js                     |  60 ++++
 app/addons/auth/routes/index.js                    |  16 +
 app/addons/auth/routes/user.js                     |  69 ++++
 app/addons/auth/stores.js                          | 165 ---------
 app/addons/auth/stores/ChangePassword.js           |  58 ++++
 app/addons/auth/stores/CreateAdminSidebarStore.js  |  35 ++
 app/addons/auth/stores/CreateAdminStore.js         |  57 +++
 app/addons/auth/stores/index.js                    |  13 +
 ...onentsSpec.js => auth.componentsSpec.react.jsx} |   6 +-
 app/addons/documents/changes/components.react.jsx  | 352 +++++++++++++++++++
 app/addons/replication/base.js                     |   8 +-
 app/app.js                                         |   6 +-
 app/core/auth.js                                   |  64 ++--
 app/core/base.js                                   |   2 +-
 app/core/couchdb/admin.js                          |  21 ++
 app/core/couchdb/index.js                          |  19 +
 app/core/couchdb/session.js                        |  32 ++
 app/core/http.js                                   |  27 ++
 app/core/session.js                                | 166 +++++++++
 34 files changed, 1654 insertions(+), 1198 deletions(-)

diff --git a/app/addons/auth/__tests__/base.test.js b/app/addons/auth/__tests__/base.test.js
deleted file mode 100644
index ae56b16..0000000
--- a/app/addons/auth/__tests__/base.test.js
+++ /dev/null
@@ -1,83 +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.
-import FauxtonAPI from "../../../core/api";
-import Auth from "../../../core/auth";
-import Base from "../base";
-import sinon from "sinon";
-
-
-describe("Auth", function () {
-  FauxtonAPI.auth = new Auth();
-  Base.initialize();
-
-  describe("failed login", function () {
-
-    it("redirects with replace: true set", function () {
-      const navigateSpy = sinon.spy(FauxtonAPI, 'navigate');
-      FauxtonAPI.router.trigger = () => {};
-      FauxtonAPI.session.isLoggedIn = function () { return false; };
-      FauxtonAPI.auth.authDeniedCb();
-      expect(navigateSpy.withArgs('/login?urlback=', {replace: true}).calledOnce).toBeTruthy();
-      FauxtonAPI.navigate.restore();
-    });
-  });
-
-  describe('auth session change', function () {
-
-    afterEach(function () {
-      FauxtonAPI.addHeaderLink.restore && FauxtonAPI.addHeaderLink.restore();
-      FauxtonAPI.session.isLoggedIn.restore && FauxtonAPI.session.isLoggedIn.restore();
-      FauxtonAPI.session.isAdminParty.restore();
-    });
-
-    it('for admin party changes title to admin party', function () {
-      const spy = sinon.spy(FauxtonAPI, 'addHeaderLink');
-      sinon.stub(FauxtonAPI.session, 'isAdminParty').returns(true);
-      FauxtonAPI.session.trigger('change');
-
-      expect(spy.calledOnce).toBeTruthy();
-      const args = spy.getCall(0).args[0];
-      expect(args.title).toMatch("Admin Party!");
-    });
-
-    it('for login changes title to Your Account', function () {
-      var spy = sinon.spy(FauxtonAPI, 'addHeaderLink');
-      sinon.stub(FauxtonAPI.session, 'isAdminParty').returns(false);
-      sinon.stub(FauxtonAPI.session, 'isLoggedIn').returns(true);
-      FauxtonAPI.session.trigger('change');
-
-      expect(spy.calledOnce).toBeTruthy();
-      var args = spy.getCall(0).args[0];
-      expect(args.title).toMatch("Your Account");
-    });
-
-    it('for login adds logout link', function () {
-      var spy = sinon.spy(FauxtonAPI, 'showLogout');
-      sinon.stub(FauxtonAPI.session, 'isAdminParty').returns(false);
-      sinon.stub(FauxtonAPI.session, 'isLoggedIn').returns(true);
-      FauxtonAPI.session.trigger('change');
-
-      expect(spy.calledOnce).toBeTruthy();
-      FauxtonAPI.showLogout.restore();
-    });
-
-    it('for logout, removes logout link', function () {
-      var spy = sinon.spy(FauxtonAPI, 'showLogin');
-      sinon.stub(FauxtonAPI.session, 'isAdminParty').returns(false);
-      sinon.stub(FauxtonAPI.session, 'isLoggedIn').returns(false);
-      FauxtonAPI.session.trigger('change');
-
-      expect(spy.calledOnce).toBeTruthy();
-      FauxtonAPI.showLogin.restore();
-    });
-  });
-});
diff --git a/app/addons/auth/actions.js b/app/addons/auth/actions.js
index 9375c7b..de926ba 100644
--- a/app/addons/auth/actions.js
+++ b/app/addons/auth/actions.js
@@ -10,148 +10,167 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 import FauxtonAPI from "../../core/api";
-import ActionTypes from "./actiontypes";
+import { session } from "../../core/couchdb";
 import ClusterStore from "../cluster/cluster.stores";
-
-var nodesStore = ClusterStore.nodesStore;
-
-var errorHandler = function (xhr, type, msg) {
-  msg = xhr;
-  if (arguments.length === 3) {
-    msg = xhr.responseJSON.reason;
-  }
-
+import ActionTypes from './actiontypes';
+
+const {
+  AUTH_CLEAR_CHANGE_PWD_FIELDS,
+  AUTH_UPDATE_CHANGE_PWD_FIELD,
+  AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD,
+  AUTH_CLEAR_CREATE_ADMIN_FIELDS,
+  AUTH_CREDS_VALID,
+  AUTH_CREDS_INVALID,
+  AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
+  AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD,
+  AUTH_SELECT_PAGE,
+  AUTH_SHOW_PASSWORD_MODAL,
+  AUTH_HIDE_PASSWORD_MODAL
+} = ActionTypes;
+
+const nodesStore = ClusterStore.nodesStore;
+
+function errorHandler({ message }) {
   FauxtonAPI.addNotification({
-    msg: msg,
-    type: 'error'
+    msg: message,
+    type: "error"
   });
 };
 
-
-function login (username, password, urlBack) {
-  var promise = FauxtonAPI.session.login(username, password);
-
-  promise.then(() => {
-    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.loggedIn });
-    if (urlBack) {
-      return FauxtonAPI.navigate(urlBack);
-    }
-    FauxtonAPI.navigate('/');
-  }, errorHandler);
+export function login(username, password, urlBack) {
+  return FauxtonAPI.session.login(username, password)
+    .then(() => {
+      FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.loggedIn });
+      if (urlBack && !urlBack.includes("login")) {
+        return FauxtonAPI.navigate(urlBack);
+      }
+      FauxtonAPI.navigate("/");
+    })
+    .catch(errorHandler);
 }
 
-function changePassword (password, passwordConfirm) {
+export function changePassword(password, passwordConfirm) {
   var nodes = nodesStore.getNodes();
-  var promise = FauxtonAPI.session.changePassword(password, passwordConfirm, nodes[0].node);
-
-  promise.then(() => {
-    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.changePassword });
-    FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CHANGE_PWD_FIELDS });
-  }, errorHandler);
+  var promise = FauxtonAPI.session.changePassword(
+    password,
+    passwordConfirm,
+    nodes[0].node
+  );
+
+  promise.then(
+    () => {
+      FauxtonAPI.addNotification({
+        msg: FauxtonAPI.session.messages.changePassword
+      });
+      FauxtonAPI.dispatch({ type: AUTH_CLEAR_CHANGE_PWD_FIELDS });
+    },
+    errorHandler
+  );
 }
 
-function updateChangePasswordField (value) {
+export function updateChangePasswordField(value) {
   FauxtonAPI.dispatch({
-    type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_FIELD,
+    type: AUTH_UPDATE_CHANGE_PWD_FIELD,
     value: value
   });
 }
 
-function updateChangePasswordConfirmField (value) {
+export function updateChangePasswordConfirmField(value) {
   FauxtonAPI.dispatch({
-    type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD,
+    type: AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD,
     value: value
   });
 }
 
-function createAdmin (username, password, loginAfter) {
-  var nodes = nodesStore.getNodes();
-  var promise = FauxtonAPI.session.createAdmin(username, password, loginAfter, nodes[0].node);
-
-  promise.then(() => {
-    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.adminCreated });
-    if (loginAfter) {
-      FauxtonAPI.navigate('/');
-    } else {
-      FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CREATE_ADMIN_FIELDS });
+export const createAdmin = (username, password, loginAfter) => {
+  const nodes = nodesStore.getNodes();
+  FauxtonAPI.session.createAdmin(
+    username,
+    password,
+    loginAfter,
+    nodes[0].node
+  )
+  .then(
+    () => {
+      FauxtonAPI.addNotification({
+        msg: FauxtonAPI.session.messages.adminCreated
+      });
+      if (loginAfter) {
+        return FauxtonAPI.navigate("/");
+      } else {
+        FauxtonAPI.dispatch({ type: AUTH_CLEAR_CREATE_ADMIN_FIELDS });
+      }
+    },
+    (xhr, type, msg) => {
+      msg = xhr;
+      if (arguments.length === 3) {
+        msg = xhr.responseJSON.reason;
+      }
+      errorHandler(
+        `${FauxtonAPI.session.messages.adminCreationFailedPrefix} ${msg}`
+      );
     }
-  }, (xhr, type, msg) => {
-    msg = xhr;
-    if (arguments.length === 3) {
-      msg = xhr.responseJSON.reason;
-    }
-    errorHandler(FauxtonAPI.session.messages.adminCreationFailedPrefix + ' ' + msg);
-  });
-}
+  );
+};
 
 // simple authentication method - does nothing other than check creds
-function authenticate (username, password, onSuccess) {
-  $.ajax({
-    cache: false,
-    type: 'POST',
-    url: '/_session',
-    dataType: 'json',
-    data: { name: username, password: password }
-  }).then(() => {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_CREDS_VALID,
-      options: { username: username, password: password }
-    });
-    hidePasswordModal();
-    onSuccess(username, password);
-  }, () => {
-    FauxtonAPI.addNotification({
-      msg: 'Your password is incorrect.',
-      type: 'error',
-      clear: true
-    });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_CREDS_INVALID,
-      options: { username: username, password: password }
-    });
-  });
+export function authenticate(username, password, onSuccess) {
+  session
+    .create({
+      name: username,
+      password: password
+    })
+    .then(
+      () => {
+        FauxtonAPI.dispatch({
+          type: AUTH_CREDS_VALID,
+          options: {
+            username,
+            password
+          }
+        });
+        hidePasswordModal();
+        onSuccess(username, password);
+      },
+      () => {
+        FauxtonAPI.addNotification({
+          msg: "Your password is incorrect.",
+          type: "error",
+          clear: true
+        });
+        FauxtonAPI.dispatch({
+          type: AUTH_CREDS_INVALID,
+          options: { username: username, password: password }
+        });
+      }
+    );
 }
 
-function updateCreateAdminUsername (value) {
+export function updateCreateAdminUsername(value) {
   FauxtonAPI.dispatch({
-    type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
+    type: AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
     value: value
   });
 }
 
-function updateCreateAdminPassword (value) {
+export function updateCreateAdminPassword(value) {
   FauxtonAPI.dispatch({
-    type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD,
+    type: AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD,
     value: value
   });
 }
 
-function selectPage (page) {
+export function selectPage(page) {
   FauxtonAPI.dispatch({
-    type: ActionTypes.AUTH_SELECT_PAGE,
+    type: AUTH_SELECT_PAGE,
     page: page
   });
 }
 
-function showPasswordModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.AUTH_SHOW_PASSWORD_MODAL });
+export function showPasswordModal() {
+  FauxtonAPI.dispatch({ type: AUTH_SHOW_PASSWORD_MODAL });
 }
 
-function hidePasswordModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.AUTH_HIDE_PASSWORD_MODAL });
+export function hidePasswordModal() {
+  FauxtonAPI.dispatch({ type: AUTH_HIDE_PASSWORD_MODAL });
 }
-
-
-export default {
-  login,
-  changePassword,
-  updateChangePasswordField,
-  updateChangePasswordConfirmField,
-  createAdmin,
-  authenticate,
-  updateCreateAdminUsername,
-  updateCreateAdminPassword,
-  selectPage,
-  showPasswordModal,
-  hidePasswordModal
-};
diff --git a/app/addons/auth/actiontypes.js b/app/addons/auth/actiontypes.js
index 937113a..cd7667f 100644
--- a/app/addons/auth/actiontypes.js
+++ b/app/addons/auth/actiontypes.js
@@ -1,4 +1,4 @@
-// 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
 //
diff --git a/app/addons/auth/assets/less/auth.less b/app/addons/auth/assets/less/auth.less
index 5fa1e1f..0a0d24d 100644
--- a/app/addons/auth/assets/less/auth.less
+++ b/app/addons/auth/assets/less/auth.less
@@ -13,7 +13,7 @@
 @import "../../../../../assets/less/variables.less";
 
 /*#primary-navbar .navbar nav .nav li a#user-create-admin{
-  background-color: @brandHighlight;
+  background-color: @navBG;
 }*/
 
 .sidenav header {
diff --git a/app/addons/auth/base.js b/app/addons/auth/base.js
index c7c32c7..f9c6b63 100644
--- a/app/addons/auth/base.js
+++ b/app/addons/auth/base.js
@@ -12,88 +12,67 @@
 
 import app from "../../app";
 import FauxtonAPI from "../../core/api";
-import Auth from "./routes";
+import Session from  "../../core/session";
+import RouteObjects from './routes';
 import "./assets/less/auth.less";
 
-Auth.session = new Auth.Session();
+const Auth = {
+  session: new Session()
+};
+
 FauxtonAPI.setSession(Auth.session);
 app.session = Auth.session;
 
-
-function cleanupAuthSection () {
+const cleanupAuthSection = () => {
   FauxtonAPI.removeHeaderLink({ id: 'auth', bottomNav: true });
-}
-
-Auth.initialize = function () {
-
-  Auth.session.on('change', function () {
-    const session = Auth.session;
-    let link;
-
-    if (session.isAdminParty()) {
-      link = {
-        id: 'auth',
-        title: 'Admin Party!',
-        href: '#/createAdmin',
-        icon: 'fonticon-user',
-        bottomNav: true
-      };
-
-      cleanupAuthSection();
-      FauxtonAPI.addHeaderLink(link);
-      FauxtonAPI.hideLogin();
-
-    } else if (session.isLoggedIn()) {
-      link = {
-        id: 'auth',
-        title: 'Your Account',
-        href: '#/changePassword',
-        icon: 'fonticon-user',
-        bottomNav: true
-      };
-
-      cleanupAuthSection();
-      FauxtonAPI.addHeaderLink(link);
-      FauxtonAPI.showLogout();
-    } else {
-      cleanupAuthSection();
-      FauxtonAPI.showLogin();
-    }
-
-  });
-
-  Auth.session.fetchUser().then(function () {
-    Auth.session.trigger('change');
-  });
-
-  var auth = function (session, roles) {
-    var deferred = $.Deferred();
-
-    if (session.isAdminParty()) {
-      session.trigger('authenticated');
-      deferred.resolve();
-    } else if (session.matchesRoles(roles)) {
-      session.trigger('authenticated');
-      deferred.resolve();
-    } else {
-      deferred.reject();
-    }
-
-    return [deferred];
-  };
-
-  var authDenied = function () {
-    var url = window.location.hash.replace('#', '');
-    var pattern = /login\?urlback=/g;
-
-    if (pattern.test(url)) {
-      url = url.replace('login?urlback=', '');
-    }
-    FauxtonAPI.navigate('/login?urlback=' + url, { replace: true });
-  };
-
-  FauxtonAPI.auth.registerAuth(auth);
-  FauxtonAPI.auth.registerAuthDenied(authDenied);
 };
 
-export default Auth;
+export default ({
+  initialize: () => {
+    FauxtonAPI.addHeaderLink({
+      id: 'auth',
+      title: 'Login',
+      href: '#/login',
+      icon: 'fonticon-user',
+      bottomNav: true
+    });
+
+    Auth.session.onChange(() => {
+      const session = Auth.session;
+      let link;
+
+      if (session.isAdminParty()) {
+        link = {
+          id: 'auth',
+          title: 'Admin Party!',
+          href: '#/createAdmin',
+          icon: 'fonticon-user',
+          bottomNav: true
+        };
+
+        cleanupAuthSection();
+        FauxtonAPI.addHeaderLink(link);
+        FauxtonAPI.hideLogin();
+
+      } else if (session.isLoggedIn()) {
+        link = {
+          id: 'auth',
+          title: 'Your Account',
+          href: '#/changePassword',
+          icon: 'fonticon-user',
+          bottomNav: true
+        };
+
+        cleanupAuthSection();
+        FauxtonAPI.addHeaderLink(link);
+        FauxtonAPI.showLogout();
+      } else {
+        cleanupAuthSection();
+        FauxtonAPI.showLogin();
+      }
+    });
+
+    Auth.session.fetchUser();
+  },
+  RouteObjects
+});
diff --git a/app/addons/auth/components.js b/app/addons/auth/components.js
deleted file mode 100644
index 36ac0d3..0000000
--- a/app/addons/auth/components.js
+++ /dev/null
@@ -1,384 +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.
-
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import React from "react";
-import ReactDOM from "react-dom";
-import AuthStores from "./stores";
-import AuthActions from "./actions";
-import { Modal } from 'react-bootstrap';
-import Components from '../components/react-components';
-
-var changePasswordStore = AuthStores.changePasswordStore;
-var createAdminStore = AuthStores.createAdminStore;
-var createAdminSidebarStore = AuthStores.createAdminSidebarStore;
-const {ConfirmButton} = Components;
-
-
-var LoginForm = React.createClass({
-  propTypes: {
-    urlBack: React.PropTypes.string.isRequired
-  },
-
-  getInitialState: function () {
-    return {
-      username: '',
-      password: ''
-    };
-  },
-
-  getDefaultProps: function () {
-    return {
-      urlBack: '',
-
-      // for testing purposes only
-      testBlankUsername: null,
-      testBlankPassword: null
-    };
-  },
-
-  onInputChange: function (e) {
-    var change = (e.target.name === 'name') ? { username: e.target.value } : { password: e.target.value };
-    this.setState(change);
-  },
-
-  submit: function (e) {
-    e.preventDefault();
-
-    if (!this.checkUnrecognizedAutoFill()) {
-      this.login(this.state.username, this.state.password);
-    }
-  },
-
-  // Safari has a bug where autofill doesn't trigger a change event. This checks for the condition where the state
-  // and form fields have a mismatch. See: https://issues.apache.org/jira/browse/COUCHDB-2829
-  checkUnrecognizedAutoFill: function () {
-    if (this.state.username !== '' || this.state.password !== '') {
-      return false;
-    }
-    var username = (this.props.testBlankUsername) ? this.props.testBlankUsername : ReactDOM.findDOMNode(this.refs.username).value;
-    var password = (this.props.testBlankPassword) ? this.props.testBlankPassword : ReactDOM.findDOMNode(this.refs.password).value;
-    this.setState({ username: username, password: password }); // doesn't set immediately, hence separate login() call
-    this.login(username, password);
-
-    return true;
-  },
-
-  login: function (username, password) {
-    AuthActions.login(username, password, this.props.urlBack);
-  },
-
-  componentDidMount: function () {
-    ReactDOM.findDOMNode(this.refs.username).focus();
-  },
-
-  render: function () {
-    return (
-      <div className="couch-login-wrapper">
-        <div className="row-fluid">
-          <div className="span12">
-            <form id="login" onSubmit={this.submit}>
-              <p className="help-block">
-                Enter your username and password.
-              </p>
-              <input id="username" type="text" name="name" ref="username" placeholder="Username" size="24"
-                onChange={this.onInputChange} value={this.state.username} />
-              <br/>
-              <input id="password" type="password" name="password" ref="password" placeholder="Password" size="24"
-                onChange={this.onInputChange} value={this.state.password} />
-              <br/>
-              <button id="submit" className="btn btn-primary" type="submit">Log In</button>
-            </form>
-          </div>
-        </div>
-      </div>
-    );
-  }
-});
-
-
-var ChangePasswordForm = React.createClass({
-  getInitialState: function () {
-    return this.getStoreState();
-  },
-
-  getStoreState: function () {
-    return {
-      password: changePasswordStore.getChangePassword(),
-      passwordConfirm: changePasswordStore.getChangePasswordConfirm()
-    };
-  },
-
-  onChange: function () {
-    this.setState(this.getStoreState());
-  },
-
-  onChangePassword: function (e) {
-    AuthActions.updateChangePasswordField(e.target.value);
-  },
-
-  onChangePasswordConfirm: function (e) {
-    AuthActions.updateChangePasswordConfirmField(e.target.value);
-  },
-
-  componentDidMount: function () {
-    ReactDOM.findDOMNode(this.refs.password).focus();
-    changePasswordStore.on('change', this.onChange, this);
-  },
-
-  componentWillUnmount: function () {
-    changePasswordStore.off('change', this.onChange);
-  },
-
-  changePassword: function (e) {
-    e.preventDefault();
-    AuthActions.changePassword(this.state.password, this.state.passwordConfirm);
-  },
-
-  render: function () {
-    return (
-      <div className="auth-page">
-        <form id="change-password" onSubmit={this.changePassword}>
-          <p>
-            Enter and confirm a new password.
-          </p>
-
-          <input id="password" type="password" ref="password" name="password" placeholder="Password"
-            size="24" onChange={this.onChangePassword} value={this.state.password} />
-          <br />
-          <input id="password-confirm" type="password" name="password_confirm" placeholder= "Password Confirmation"
-            size="24" onChange={this.onChangePasswordConfirm} value={this.state.passwordConfirm} />
-
-          <br />
-          <p>
-            <button type="submit" className="btn btn-primary"><i className="icon icon-ok-circle" /> Change Password</button>
-          </p>
-        </form>
-      </div>
-    );
-  }
-});
-
-
-var CreateAdminForm = React.createClass({
-  propTypes: {
-    loginAfter: React.PropTypes.bool.isRequired
-  },
-
-  getInitialState: function () {
-    return this.getStoreState();
-  },
-
-  getStoreState: function () {
-    return {
-      username: createAdminStore.getUsername(),
-      password: createAdminStore.getPassword()
-    };
-  },
-
-  onChange: function () {
-    this.setState(this.getStoreState());
-  },
-
-  getDefaultProps: function () {
-    return {
-      loginAfter: ''
-    };
-  },
-
-  onChangeUsername: function (e) {
-    AuthActions.updateCreateAdminUsername(e.target.value);
-  },
-
-  onChangePassword: function (e) {
-    AuthActions.updateCreateAdminPassword(e.target.value);
-  },
-
-  componentDidMount: function () {
-    ReactDOM.findDOMNode(this.refs.username).focus();
-    createAdminStore.on('change', this.onChange, this);
-  },
-
-  componentWillUnmount: function () {
-    createAdminStore.off('change', this.onChange);
-  },
-
-  createAdmin: function (e) {
-    e.preventDefault();
-    AuthActions.createAdmin(this.state.username, this.state.password, this.props.loginAfter);
-  },
-
-  render: function () {
-    return (
-      <div className="auth-page">
-        <p>
-          Before a server admin is configured, all client connections have admin privileges. <strong>If HTTP access is open to non-trusted users, create an admin account to prevent data
-          loss.</strong>
-        </p>
-        <p>
-          Connections with Admin privileges can create and destroy databases, install and update _design documents, run
-          the test suite, and modify the CouchDB configuration.
-        </p>
-        <p>
-          Connections without Admin privileges have read and write access to all databases controlled by validation functions. CouchDB can be configured to block anonymous connections.
-        </p>
-        <form id="create-admin-form" onSubmit={this.createAdmin}>
-          <input id="username" type="text" ref="username" name="name" placeholder="Username" size="24"
-            onChange={this.onChangeUsername} />
-          <br/>
-          <input id="password" type="password" name="password" placeholder= "Password" size="24"
-            onChange={this.onChangePassword} />
-          <p>
-          <button type="submit" id="create-admin" className="btn btn-primary"><i className="icon icon-ok-circle" /> Grant Admin Privileges</button>
-          </p>
-        </form>
-      </div>
-    );
-  }
-});
-
-
-var CreateAdminSidebar = React.createClass({
-  getInitialState: function () {
-    return this.getStoreState();
-  },
-
-  getStoreState: function () {
-    return {
-      selectedPage: createAdminSidebarStore.getSelectedPage()
-    };
-  },
-
-  onChange: function () {
-    this.setState(this.getStoreState());
-  },
-
-  componentDidMount: function () {
-    createAdminSidebarStore.on('change', this.onChange, this);
-  },
-
-  componentWillUnmount: function () {
-    createAdminSidebarStore.off('change', this.onChange);
-  },
-
-  selectPage: function (e) {
-    var newPage = e.target.href.split('#')[1];
-    AuthActions.selectPage(newPage);
-  },
-
-  render: function () {
-    var user = FauxtonAPI.session.user();
-    var userName = _.isNull(user) ? '' : FauxtonAPI.session.user().name;
-
-    return (
-      <div className="sidenav">
-        <header className="row-fluid">
-          <h3>{userName}</h3>
-        </header>
-        <ul className="nav nav-list" onClick={this.selectPage}>
-          <li className={this.state.selectedPage === 'changePassword' ? 'active' : ''} data-page="changePassword">
-            <a href="#changePassword">Change Password</a>
-          </li>
-          <li className={this.state.selectedPage === 'addAdmin' ? 'active' : ''} data-page="addAdmin">
-            <a href="#addAdmin">Create Admins</a>
-          </li>
-        </ul>
-      </div>
-    );
-  }
-});
-
-
-class PasswordModal extends React.Component {
-  constructor (props) {
-    super(props);
-    this.state = {
-      password: ''
-    };
-    this.authenticate = this.authenticate.bind(this);
-    this.onKeyPress = this.onKeyPress.bind(this);
-  }
-
-  // clicking <Enter> should submit the form
-  onKeyPress (e) {
-    if (e.key === 'Enter') {
-      this.authenticate();
-    }
-  }
-
-  // default authentication function. This can be overridden via props if you want to do something different
-  authenticate () {
-    const username = app.session.get('userCtx').name; // yuck. But simplest for now until logging in publishes the user data
-    this.props.onSubmit(username, this.state.password, this.props.onSuccess);
-  }
-
-  render () {
-    const {visible, onClose, submitBtnLabel, headerTitle, modalMessage} = this.props;
-    if (!this.props.visible) {
-      return null;
-    }
-
-    return (
-      <Modal dialogClassName="enter-password-modal" show={visible} onHide={() => onClose()}>
-        <Modal.Header closeButton={true}>
-          <Modal.Title>{headerTitle}</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          {modalMessage}
-          <input
-            style={{width: "385px"}}
-            type="password"
-            className="password-modal-input"
-            placeholder="Enter your password"
-            autoFocus={true}
-            value={this.state.password}
-            onChange={(e) => this.setState({ password: e.target.value })}
-            onKeyPress={this.onKeyPress}
-            />
-        </Modal.Body>
-        <Modal.Footer>
-          <a className="cancel-link" onClick={() => onClose()}>Cancel</a>
-          <ConfirmButton
-            text={submitBtnLabel}
-            onClick={this.authenticate}
-          />
-        </Modal.Footer>
-      </Modal>
-    );
-  }
-}
-PasswordModal.propTypes = {
-  visible: React.PropTypes.bool.isRequired,
-  modalMessage: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
-  onSubmit: React.PropTypes.func.isRequired,
-  onClose: React.PropTypes.func.isRequired,
-  submitBtnLabel: React.PropTypes.string
-};
-PasswordModal.defaultProps = {
-  headerTitle: "Enter Password",
-  visible: false,
-  modalMessage: '',
-  onClose: AuthActions.hidePasswordModal,
-  onSubmit: AuthActions.authenticate,
-  onSuccess: () => {},
-  submitBtnLabel: 'Continue'
-};
-
-
-export default {
-  LoginForm,
-  ChangePasswordForm,
-  CreateAdminForm,
-  CreateAdminSidebar,
-  PasswordModal
-};
diff --git a/app/addons/auth/components/changepasswordform.js b/app/addons/auth/components/changepasswordform.js
new file mode 100644
index 0000000..5d54917
--- /dev/null
+++ b/app/addons/auth/components/changepasswordform.js
@@ -0,0 +1,92 @@
+// 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.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import { changePasswordStore } from "./../stores";
+import {
+  updateChangePasswordField,
+  updateChangePasswordConfirmField,
+  changePassword
+} from "./../actions";
+
+export default class ChangePasswordForm extends React.Component {
+  constructor() {
+    super();
+    this.state = this.getStoreState();
+  }
+  getStoreState() {
+    return {
+      password: changePasswordStore.getChangePassword(),
+      passwordConfirm: changePasswordStore.getChangePasswordConfirm()
+    };
+  }
+  onChange() {
+    this.setState(this.getStoreState());
+  }
+  onChangePassword(e) {
+    updateChangePasswordField(e.target.value);
+  }
+  onChangePasswordConfirm(e) {
+    updateChangePasswordConfirmField(e.target.value);
+  }
+  componentDidMount() {
+    ReactDOM.findDOMNode(this.refs.password).focus();
+    changePasswordStore.on("change", this.onChange, this);
+  }
+  componentWillUnmount() {
+    changePasswordStore.off("change", this.onChange);
+  }
+  changePassword(e) {
+    e.preventDefault();
+    changePassword(this.state.password, this.state.passwordConfirm);
+  }
+  render() {
+    return (
+      <div className="auth-page">
+        <h3>Change Password</h3>
+
+        <form id="change-password" onSubmit={this.changePassword.bind(this)}>
+          <p>
+            Enter your new password.
+          </p>
+
+          <input
+            id="password"
+            type="password"
+            ref="password"
+            name="password"
+            placeholder="Password"
+            size="24"
+            onChange={this.onChangePassword.bind(this)}
+            value={this.state.password}
+          />
+          <br />
+          <input
+            id="password-confirm"
+            type="password"
+            name="password_confirm"
+            placeholder="Verify Password"
+            size="24"
+            onChange={this.onChangePasswordConfirm.bind(this)}
+            value={this.state.passwordConfirm}
+          />
+
+          <br />
+          <p>
+            <button type="submit" className="btn btn-primary">Change</button>
+          </p>
+        </form>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/auth/components/createadminform.js b/app/addons/auth/components/createadminform.js
new file mode 100644
index 0000000..47c70b5
--- /dev/null
+++ b/app/addons/auth/components/createadminform.js
@@ -0,0 +1,119 @@
+// 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.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import { createAdminStore } from "./../stores";
+import {
+  updateCreateAdminUsername,
+  updateCreateAdminPassword,
+  createAdmin
+} from "./../actions";
+
+class CreateAdminForm extends React.Component {
+  getInitialState() {
+    return this.getStoreState();
+  }
+  getStoreState() {
+    return {
+      username: createAdminStore.getUsername(),
+      password: createAdminStore.getPassword()
+    };
+  }
+  onChange() {
+    this.setState(this.getStoreState());
+  }
+  getDefaultProps() {
+    return {
+      loginAfter: ""
+    };
+  }
+  onChangeUsername(e) {
+    updateCreateAdminUsername(e.target.value);
+  }
+  onChangePassword(e) {
+    updateCreateAdminPassword(e.target.value);
+  }
+  componentDidMount() {
+    ReactDOM.findDOMNode(this.refs.username).focus();
+    createAdminStore.on("change", this.onChange, this);
+  }
+  componentWillUnmount() {
+    createAdminStore.off("change", this.onChange);
+  }
+  createAdmin(e) {
+    e.preventDefault();
+    createAdmin(
+      this.state.username,
+      this.state.password,
+      this.props.loginAfter
+    );
+  }
+  render() {
+    return (
+      <div className="auth-page">
+        <h3>Create Admins</h3>
+
+        <p>
+          Before a server admin is configured, all clients have admin privileges. This is fine when
+
+          HTTP access is restricted to trusted users. <strong>
+            If end-users will be accessing this
+            CouchDB, you must create an admin account to prevent accidental (or malicious) data
+
+            loss.
+          </strong>
+        </p>
+        <p>
+          Server admins can create and destroy databases, install and update _design documents, run
+
+          the test suite, and edit all aspects of CouchDB configuration.
+        </p>
+
+        <form id="create-admin-form" onSubmit={this.createAdmin}>
+          <input
+            id="username"
+            type="text"
+            ref="username"
+            name="name"
+            placeholder="Username"
+            size="24"
+            onChange={this.onChangeUsername}
+          />
+          <br />
+          <input
+            id="password"
+            type="password"
+            name="password"
+            placeholder="Password"
+            size="24"
+            onChange={this.onChangePassword}
+          />
+          <p>
+            Non-admin users have read and write access to all databases, which
+            are controlled by validatio. CouchDB can be configured to block all
+            access to anonymous users.
+          </p>
+          <button type="submit" id="create-admin" className="btn btn-primary">
+            Create Admin
+          </button>
+        </form>
+      </div>
+    );
+  }
+}
+
+CreateAdminForm.propTypes = {
+  loginAfter: React.PropTypes.bool.isRequired
+};
+
+export default CreateAdminForm;
diff --git a/app/addons/auth/components/createadminsidebar.js b/app/addons/auth/components/createadminsidebar.js
new file mode 100644
index 0000000..9b954e9
--- /dev/null
+++ b/app/addons/auth/components/createadminsidebar.js
@@ -0,0 +1,61 @@
+// 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.
+
+import React from 'react';
+import { selectPage } from './../actions';
+import { createAdminSidebarStore } from './../stores';
+import FauxtonAPI from "../../../core/api";
+
+export default class CreateAdminSidebar extends React.Component {
+  constructor() {
+    super();
+    this.state = this.getStoreState();
+  }
+  getStoreState() {
+    return {
+      selectedPage: createAdminSidebarStore.getSelectedPage()
+    };
+  }
+  onChange() {
+    this.setState(this.getStoreState());
+  }
+  componentDidMount() {
+    createAdminSidebarStore.on('change', this.onChange, this);
+  }
+  componentWillUnmount() {
+    createAdminSidebarStore.off('change', this.onChange);
+  }
+  selectPage(e) {
+    var newPage = e.target.href.split('#')[1];
+    selectPage(newPage);
+  }
+  render() {
+    var user = FauxtonAPI.session.user;
+    var userName = _.isNull(user) ? '' : FauxtonAPI.session.user.name;
+
+    return (
+      <div className="sidenav">
+        <header className="row-fluid">
+          <h3>{userName}</h3>
+        </header>
+        <ul className="nav nav-list" onClick={this.selectPage}>
+          <li className={this.state.selectedPage === 'changePassword' ? 'active' : ''} data-page="changePassword">
+            <a href="#changePassword">Change Password</a>
+          </li>
+          <li className={this.state.selectedPage === 'addAdmin' ? 'active' : ''} data-page="addAdmin">
+            <a href="#addAdmin">Create Admins</a>
+          </li>
+        </ul>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/auth/components/index.js b/app/addons/auth/components/index.js
new file mode 100644
index 0000000..d322903
--- /dev/null
+++ b/app/addons/auth/components/index.js
@@ -0,0 +1,25 @@
+// 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.
+
+import LoginForm from './loginform.js';
+import PasswordModal from './passwordmodal.js';
+import CreateAdminSidebar from './createadminsidebar.js';
+import CreateAdminForm from './createadminform.js';
+import ChangePasswordForm from './changepasswordform.js';
+
+export default ({
+  LoginForm,
+  PasswordModal,
+  CreateAdminSidebar,
+  CreateAdminForm,
+  ChangePasswordForm
+});
diff --git a/app/addons/auth/components/loginform.js b/app/addons/auth/components/loginform.js
new file mode 100644
index 0000000..3518225
--- /dev/null
+++ b/app/addons/auth/components/loginform.js
@@ -0,0 +1,110 @@
+// 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.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import { login } from "./../actions";
+
+class LoginForm extends React.Component {
+  constructor() {
+    super();
+    this.state = {
+      username: "",
+      password: ""
+    };
+  }
+  onInputChange(e) {
+    let change = e.target.name === "name"
+      ? { username: e.target.value }
+      : { password: e.target.value };
+    this.setState(change);
+  }
+  submit(e) {
+    e.preventDefault();
+    if (!this.checkUnrecognizedAutoFill()) {
+      this.login(this.state.username, this.state.password);
+    }
+  }
+  // Safari has a bug where autofill doesn't trigger a change event. This checks for the condition where the state
+  // and form fields have a mismatch. See: https://issues.apache.org/jira/browse/COUCHDB-2829
+  checkUnrecognizedAutoFill() {
+    if (this.state.username !== "" || this.state.password !== "") {
+      return false;
+    }
+    var username = this.props.testBlankUsername
+      ? this.props.testBlankUsername
+      : ReactDOM.findDOMNode(this.refs.username).value;
+    var password = this.props.testBlankPassword
+      ? this.props.testBlankPassword
+      : ReactDOM.findDOMNode(this.refs.password).value;
+    this.setState({ username: username, password: password }); // doesn't set immediately, hence separate login() call
+    this.login(username, password);
+
+    return true;
+  }
+  login(username, password) {
+    login(username, password, this.props.urlBack);
+  }
+  componentDidMount() {
+    ReactDOM.findDOMNode(this.refs.username).focus();
+  }
+  render() {
+    return (
+      <div className="couch-login-wrapper">
+        <div className="row-fluid">
+          <div className="span12">
+            <form id="login" onSubmit={this.submit.bind(this)}>
+              <p className="help-block">
+                Enter your username and password.
+              </p>
+              <input
+                id="username"
+                type="text"
+                name="name"
+                ref="username"
+                placeholder="Username"
+                size="24"
+                onChange={this.onInputChange.bind(this)}
+                value={this.state.username}
+              />
+              <br />
+              <input
+                id="password"
+                type="password"
+                name="password"
+                ref="password"
+                placeholder="Password"
+                size="24"
+                onChange={this.onInputChange.bind(this)}
+                value={this.state.password}
+              />
+              <br />
+              <button id="submit" className="btn btn-success" type="submit">
+                Log In
+              </button>
+            </form>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+LoginForm.defaultProps = {
+   urlBack: ""
+};
+
+LoginForm.propTypes = {
+  urlBack: React.PropTypes.string.isRequired
+};
+
+export default LoginForm;
diff --git a/app/addons/auth/components/passwordmodal.js b/app/addons/auth/components/passwordmodal.js
new file mode 100644
index 0000000..48f7784
--- /dev/null
+++ b/app/addons/auth/components/passwordmodal.js
@@ -0,0 +1,107 @@
+// 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.
+
+import React from "react";
+import { Modal } from "react-bootstrap";
+import { hidePasswordModal, authenticate } from "./../actions";
+import Components from "../../components/react-components";
+import app from "../../../app";
+
+class PasswordModal extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      password: ""
+    };
+    this.authenticate = this.authenticate.bind(this);
+    this.onKeyPress = this.onKeyPress.bind(this);
+  }
+
+  // clicking <Enter> should submit the form
+  onKeyPress(e) {
+    if (e.key === "Enter") {
+      this.authenticate();
+    }
+  }
+  // default authentication function. This can be overridden via props if you want to do something different
+  authenticate() {
+    const username = app.session.user.name; // yuck. But simplest for now until logging in publishes the user data
+    this.props.onSubmit(username, this.state.password, this.props.onSuccess);
+  }
+  render() {
+    const {
+      visible,
+      onClose,
+      submitBtnLabel,
+      headerTitle,
+      modalMessage
+    } = this.props;
+    if (!this.props.visible) {
+      return null;
+    }
+
+    return (
+      <Modal
+        dialogClassName="enter-password-modal"
+        show={visible}
+        onHide={() => onClose()}
+      >
+        <Modal.Header closeButton={true}>
+          <Modal.Title>{headerTitle}</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          {modalMessage}
+          <input
+            style={{ width: "385px" }}
+            type="password"
+            className="password-modal-input"
+            placeholder="Enter your password"
+            autoFocus={true}
+            value={this.state.password}
+            onChange={e => this.setState({ password: e.target.value })}
+            onKeyPress={this.onKeyPress}
+          />
+        </Modal.Body>
+        <Modal.Footer>
+          <a className="cancel-link" onClick={() => onClose()}>Cancel</a>
+          <Components.ConfirmButton
+            text={submitBtnLabel}
+            onClick={this.authenticate}
+          />
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
+
+PasswordModal.propTypes = {
+  visible: React.PropTypes.bool.isRequired,
+  modalMessage: React.PropTypes.oneOfType([
+    React.PropTypes.string,
+    React.PropTypes.element
+  ]),
+  onSubmit: React.PropTypes.func.isRequired,
+  onClose: React.PropTypes.func.isRequired,
+  submitBtnLabel: React.PropTypes.string
+};
+
+PasswordModal.defaultProps = {
+  headerTitle: "Enter Password",
+  visible: false,
+  modalMessage: "",
+  onClose: hidePasswordModal,
+  onSubmit: authenticate,
+  onSuccess: () => {},
+  submitBtnLabel: "Continue"
+};
+
+export default PasswordModal;
diff --git a/app/addons/auth/layout.js b/app/addons/auth/layout.js
index b6c8291..cc9a609 100644
--- a/app/addons/auth/layout.js
+++ b/app/addons/auth/layout.js
@@ -15,7 +15,11 @@ import {OnePane, OnePaneContent} from '../components/layouts';
 import {Breadcrumbs} from '../components/header-breadcrumbs';
 import Components from "./components";
 
-const { CreateAdminForm, ChangePasswordForm } = Components;
+const {
+  CreateAdminForm,
+  ChangePasswordForm,
+  CreateAdminSidebar
+} = Components;
 
 export const OnePaneHeader = ({crumbs}) => {
   return (
@@ -53,7 +57,7 @@ export const AdminLayout = ({crumbs, changePassword}) => {
       </OnePaneHeader>
       <div className="template-content flex-body flex-layout flex-row">
         <div id="sidebar-content">
-          <Components.CreateAdminSidebar />
+          <CreateAdminSidebar />
         </div>
         <div id="dashboard-content" className="flex-body">
           {content}
diff --git a/app/addons/auth/resources.js b/app/addons/auth/resources.js
deleted file mode 100644
index 5c4a921..0000000
--- a/app/addons/auth/resources.js
+++ /dev/null
@@ -1,208 +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.
-
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import CouchdbSession from "../../core/couchdbSession";
-
-var Auth = new FauxtonAPI.addon();
-
-
-var Admin = Backbone.Model.extend({
-
-  initialize: function (props, options) {
-    this.node = options.node;
-  },
-
-  url: function () {
-    if (!this.node) {
-      throw new Error('no node set');
-    }
-
-    return app.host + '/_node/' + this.node + '/_config/admins/' + this.get('name');
-  },
-
-  isNew: function () { return false; },
-
-  sync: function (method, model) {
-    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);
-  }
-});
-
-Auth.Session = CouchdbSession.Session.extend({
-  url: app.host + '/_session',
-
-  initialize: function (options) {
-    if (!options) { options = {}; }
-
-    _.bindAll(this);
-
-    this.messages = _.extend({},  {
-        missingCredentials: 'Username or password cannot be blank.',
-        loggedIn: 'You have been logged in.',
-        adminCreated: 'CouchDB admin created',
-        changePassword: 'Your password has been updated.',
-        adminCreationFailedPrefix: 'Could not create admin.'
-      }, options.messages);
-  },
-
-  isAdminParty: function () {
-    var userCtx = this.get('userCtx');
-
-
-    if (!userCtx.name && userCtx.roles.indexOf("_admin") > -1) {
-      return true;
-    }
-
-    return false;
-  },
-
-  isLoggedIn: function () {
-    var userCtx = this.get('userCtx');
-
-    if (!userCtx) { return false;}
-    if (userCtx.name) {
-      return true;
-    }
-
-    return false;
-  },
-
-  userRoles: function () {
-    var user = this.user();
-
-    if (user && user.roles) {
-      if (user.roles.indexOf('fx_loggedIn') === -1) {
-        user.roles.push('fx_loggedIn');
-      }
-
-      return user.roles;
-    }
-
-    return [];
-  },
-
-  matchesRoles: function (roles) {
-    if (roles.length === 0) {
-      return true;
-    }
-
-    var numberMatchingRoles = _.intersection(this.userRoles(), roles).length;
-
-    if (numberMatchingRoles > 0) {
-      return true;
-    }
-
-    return false;
-  },
-
-  validateUser: function (username, password, msg) {
-    if (_.isEmpty(username) || _.isEmpty(password)) {
-      var deferred = FauxtonAPI.Deferred();
-
-      deferred.rejectWith(this, [msg]);
-      return deferred;
-    }
-  },
-
-  validatePasswords: function (password, password_confirm, msg) {
-    if (_.isEmpty(password) || _.isEmpty(password_confirm) || (password !== password_confirm)) {
-      var deferred = FauxtonAPI.Deferred();
-
-      deferred.rejectWith(this, [msg]);
-      return deferred;
-    }
-  },
-
-  createAdmin: function (username, password, login, node) {
-    var errorPromise = this.validateUser(username, password, this.messages.missingCredentials);
-
-    if (errorPromise) { return errorPromise; }
-
-    var admin = new Admin({
-      name: username,
-      value: password
-    }, {node: node});
-
-    return admin.save().then(function () {
-      if (login) {
-        return this.login(username, password);
-      }
-
-      return this.fetchUser({forceFetch: true});
-
-    }.bind(this));
-  },
-
-  login: function (username, password) {
-    var errorPromise = this.validateUser(username, password, this.messages.missingCredentials);
-
-    if (errorPromise) { return errorPromise; }
-
-    return $.ajax({
-      cache: false,
-      type: "POST",
-      url: app.host + "/_session",
-      dataType: "json",
-      data: {name: username, password: password}
-    }).then(function () {
-      return this.fetchUser({forceFetch: true});
-    }.bind(this));
-  },
-
-  logout: function () {
-    var that = this;
-
-    return $.ajax({
-      type: "DELETE",
-      url: app.host + "/_session",
-      dataType: "json",
-      username : "_",
-      password : "_"
-    }).then(function () {
-      return that.fetchUser({forceFetch: true });
-    });
-  },
-
-  changePassword: function (password, confirmedPassword, node) {
-    var errorMessage = 'Passwords do not match.';
-    var errorPromise = this.validatePasswords(password, confirmedPassword, errorMessage);
-
-    if (errorPromise) { return errorPromise; }
-
-    var userName = this.get('userCtx').name;
-    var admin = new Admin({
-      name: userName,
-      value: password
-    }, {node: node});
-
-    return admin.save().then(function () {
-      return this.login(userName, password);
-    }.bind(this));
-  }
-});
-
-
-export default Auth;
diff --git a/app/addons/auth/routes.js b/app/addons/auth/routes.js
deleted file mode 100644
index 71f4bc1..0000000
--- a/app/addons/auth/routes.js
+++ /dev/null
@@ -1,119 +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.
-
-import React from 'react';
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import Auth from "./resources";
-import AuthActions from "./actions";
-import Components from "./components";
-import ClusterActions from "../cluster/cluster.actions";
-import {AuthLayout, AdminLayout} from './layout';
-
-const {LoginForm, CreateAdminForm} = Components;
-
-var AuthRouteObject = FauxtonAPI.RouteObject.extend({
-  selectedHeader: 'Login',
-
-  routes: {
-    'login?*extra': 'login',
-    'login': 'login',
-    'logout': 'logout',
-    'createAdmin': 'checkNodes',
-    'createAdmin/:node': 'createAdminForNode'
-  },
-
-  checkNodes: function () {
-    ClusterActions.navigateToNodeBasedOnNodeCount('/createAdmin/');
-  },
-
-  login: function () {
-    const crumbs = [{ name: 'Log In to CouchDB' }];
-    return <AuthLayout
-      crumbs={crumbs}
-      component={<LoginForm urlBack={app.getParams().urlback } />}
-      />;
-  },
-
-  logout: function () {
-    FauxtonAPI.addNotification({ msg: 'You have been logged out.' });
-    FauxtonAPI.session.logout().then(function () {
-      FauxtonAPI.navigate('/');
-    });
-  },
-
-  createAdminForNode: function () {
-    ClusterActions.fetchNodes();
-    const crumbs = [{ name: 'Create Admin' }];
-    return <AuthLayout
-      crumbs={crumbs}
-      component={<CreateAdminForm loginAfter={true} />}
-      />;
-  }
-});
-
-
-var UserRouteObject = FauxtonAPI.RouteObject.extend({
-  hideNotificationCenter: true,
-  hideApiBar: true,
-  selectedHeader: 'Your Account',
-
-  routes: {
-    'changePassword': {
-      route: 'checkNodesForPasswordChange',
-      roles: ['fx_loggedIn']
-    },
-    'changePassword/:node': {
-      route: 'changePassword',
-      roles: ['fx_loggedIn']
-    },
-    'addAdmin': {
-      route: 'checkNodesForAddAdmin',
-      roles: ['_admin']
-    },
-    'addAdmin/:node': {
-      route: 'addAdmin',
-      roles: ['_admin']
-    }
-  },
-
-  checkNodesForPasswordChange: function () {
-    ClusterActions.navigateToNodeBasedOnNodeCount('/changePassword/');
-  },
-
-  checkNodesForAddAdmin: function () {
-    ClusterActions.navigateToNodeBasedOnNodeCount('/addAdmin/');
-  },
-
-  changePassword: function () {
-    ClusterActions.fetchNodes();
-    AuthActions.selectPage('changePassword');
-    return <AdminLayout
-      crumbs={[{name: 'User Management'}]}
-      changePassword={true}
-      />;
-  },
-
-  addAdmin: function () {
-    ClusterActions.fetchNodes();
-    AuthActions.selectPage('addAdmin');
-    return <AdminLayout
-      crumbs={[{name: 'User Management'}]}
-      changePassword={false}
-      />;
-  },
-
-});
-
-Auth.RouteObjects = [AuthRouteObject, UserRouteObject];
-
-export default Auth;
diff --git a/app/addons/auth/routes/auth.js b/app/addons/auth/routes/auth.js
new file mode 100644
index 0000000..e72f0a1
--- /dev/null
+++ b/app/addons/auth/routes/auth.js
@@ -0,0 +1,60 @@
+// 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.
+
+import React from "react";
+import FauxtonAPI from "../../../core/api";
+import ClusterActions from "../../cluster/cluster.actions";
+import { AuthLayout } from "./../layout";
+import app from "../../../app";
+import Components from "./../components";
+
+const {
+  LoginForm,
+  CreateAdminForm
+} = Components;
+
+const crumbs = [{ name: "Log In to CouchDB" }];
+
+export default FauxtonAPI.RouteObject.extend({
+  routes: {
+    "login?*extra": "login",
+    login: "login",
+    logout: "logout",
+    createAdmin: "checkNodes",
+    "createAdmin/:node": "createAdminForNode"
+  },
+  checkNodes() {
+    ClusterActions.navigateToNodeBasedOnNodeCount("/createAdmin/");
+  },
+  login() {
+    return (
+      <AuthLayout
+        crumbs={crumbs}
+        component={<LoginForm urlBack={app.getParams().urlback} />}
+      />
+    );
+  },
+  logout() {
+    FauxtonAPI.addNotification({ msg: "You have been logged out." });
+    FauxtonAPI.session.logout().then(() => FauxtonAPI.navigate("/"));
+  },
+  createAdminForNode() {
+    ClusterActions.fetchNodes();
+    const crumbs = [{ name: "Create Admin" }];
+    return (
+      <AuthLayout
+        crumbs={crumbs}
+        component={<CreateAdminForm loginAfter={true} />}
+      />
+    );
+  }
+});
diff --git a/app/addons/auth/routes/index.js b/app/addons/auth/routes/index.js
new file mode 100644
index 0000000..da37e64
--- /dev/null
+++ b/app/addons/auth/routes/index.js
@@ -0,0 +1,16 @@
+// 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.
+
+import Auth from './auth';
+import User from './user';
+
+export default [Auth, User];
diff --git a/app/addons/auth/routes/user.js b/app/addons/auth/routes/user.js
new file mode 100644
index 0000000..083c578
--- /dev/null
+++ b/app/addons/auth/routes/user.js
@@ -0,0 +1,69 @@
+// 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.
+
+import React from "react";
+import FauxtonAPI from "../../../core/api";
+import ClusterActions from "../../cluster/cluster.actions";
+import { AdminLayout } from "./../layout";
+import { selectPage } from './../actions';
+
+export default FauxtonAPI.RouteObject.extend({
+  hideNotificationCenter: true,
+  hideApiBar: true,
+  routes: {
+    changePassword: {
+      route: "checkNodesForPasswordChange",
+      roles: ["fx_loggedIn"]
+    },
+    "changePassword/:node": {
+      route: "changePassword",
+      roles: ["fx_loggedIn"]
+    },
+    addAdmin: {
+      route: "checkNodesForAddAdmin",
+      roles: ["_admin"]
+    },
+    "addAdmin/:node": {
+      route: "addAdmin",
+      roles: ["_admin"]
+    }
+  },
+  checkNodesForPasswordChange() {
+    ClusterActions.navigateToNodeBasedOnNodeCount("/changePassword/");
+  },
+  checkNodesForAddAdmin() {
+    ClusterActions.navigateToNodeBasedOnNodeCount("/addAdmin/");
+  },
+  selectedHeader() {
+    return FauxtonAPI.session.user.name;
+  },
+  changePassword() {
+    ClusterActions.fetchNodes();
+    selectPage("changePassword");
+    return (
+      <AdminLayout
+        crumbs={[{ name: "User Management" }]}
+        changePassword={true}
+      />
+    );
+  },
+  addAdmin() {
+    ClusterActions.fetchNodes();
+    selectPage("addAdmin");
+    return (
+      <AdminLayout
+        crumbs={[{ name: "User Management" }]}
+        changePassword={false}
+      />
+    );
+  }
+});
diff --git a/app/addons/auth/stores.js b/app/addons/auth/stores.js
deleted file mode 100644
index ce43e4a..0000000
--- a/app/addons/auth/stores.js
+++ /dev/null
@@ -1,165 +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.
-
-import FauxtonAPI from "../../core/api";
-import ActionTypes from "./actiontypes";
-
-// Not thrilled with this. The sole purpose of these next two stores is because the Create Admin + Change Password
-// forms need to clear after a successful post. Since those events occur in actions.js, we need a way to tell the
-// component to update + clear the fields. That's why all this code exists.
-
-var ChangePasswordStore = FauxtonAPI.Store.extend({
-  initialize: function () {
-    this.reset();
-  },
-
-  reset: function () {
-    this._changePassword = '';
-    this._changePasswordConfirm = '';
-  },
-
-  getChangePassword: function () {
-    return this._changePassword;
-  },
-
-  getChangePasswordConfirm: function () {
-    return this._changePasswordConfirm;
-  },
-
-  setChangePassword: function (val) {
-    this._changePassword = val;
-  },
-
-  setChangePasswordConfirm: function (val) {
-    this._changePasswordConfirm = val;
-  },
-
-  dispatch: function (action) {
-    switch (action.type) {
-
-      case ActionTypes.AUTH_CLEAR_CHANGE_PWD_FIELDS:
-        this.reset();
-        this.triggerChange();
-      break;
-
-      case ActionTypes.AUTH_UPDATE_CHANGE_PWD_FIELD:
-        this.setChangePassword(action.value);
-        this.triggerChange();
-      break;
-
-      case ActionTypes.AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD:
-        this.setChangePasswordConfirm(action.value);
-        this.triggerChange();
-      break;
-
-      default:
-      return;
-    }
-  }
-});
-
-var changePasswordStore = new ChangePasswordStore();
-changePasswordStore.dispatchToken = FauxtonAPI.dispatcher.register(changePasswordStore.dispatch.bind(changePasswordStore));
-
-
-var CreateAdminStore = FauxtonAPI.Store.extend({
-  initialize: function () {
-    this.reset();
-  },
-
-  reset: function () {
-    this._username = '';
-    this._password = '';
-  },
-
-  getUsername: function () {
-    return this._username;
-  },
-
-  getPassword: function () {
-    return this._password;
-  },
-
-  setUsername: function (val) {
-    this._username = val;
-  },
-
-  setPassword: function (val) {
-    this._password = val;
-  },
-
-  dispatch: function (action) {
-    switch (action.type) {
-      case ActionTypes.AUTH_CLEAR_CREATE_ADMIN_FIELDS:
-        this.reset();
-        this.triggerChange();
-      break;
-
-      case ActionTypes.AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD:
-        this.setUsername(action.value);
-        this.triggerChange();
-      break;
-
-      case ActionTypes.AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD:
-        this.setPassword(action.value);
-        this.triggerChange();
-      break;
-
-      default:
-      return;
-    }
-  }
-});
-
-var createAdminStore = new CreateAdminStore();
-createAdminStore.dispatchToken = FauxtonAPI.dispatcher.register(createAdminStore.dispatch.bind(createAdminStore));
-
-
-var CreateAdminSidebarStore = FauxtonAPI.Store.extend({
-  initialize: function () {
-    this.reset();
-  },
-
-  reset: function () {
-    this._selectedPage = 'changePassword';
-  },
-
-  getSelectedPage: function () {
-    return this._selectedPage;
-  },
-
-  setSelectedPage: function (val) {
-    this._selectedPage = val;
-  },
-
-  dispatch: function (action) {
-    switch (action.type) {
-      case ActionTypes.AUTH_SELECT_PAGE:
-        this.setSelectedPage(action.page);
-        this.triggerChange();
-      break;
-
-      default:
-      return;
-    }
-  }
-});
-
-var createAdminSidebarStore = new CreateAdminSidebarStore();
-createAdminSidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(createAdminSidebarStore.dispatch.bind(createAdminSidebarStore));
-
-
-export default {
-  changePasswordStore: changePasswordStore,
-  createAdminStore: createAdminStore,
-  createAdminSidebarStore: createAdminSidebarStore
-};
diff --git a/app/addons/auth/stores/ChangePassword.js b/app/addons/auth/stores/ChangePassword.js
new file mode 100644
index 0000000..41d60ac
--- /dev/null
+++ b/app/addons/auth/stores/ChangePassword.js
@@ -0,0 +1,58 @@
+import FauxtonAPI from "../../../core/api";
+import ActionTypes from '../actiontypes';
+const {
+  AUTH_CLEAR_CHANGE_PWD_FIELDS,
+  AUTH_UPDATE_CHANGE_PWD_FIELD,
+  AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD
+} = ActionTypes;
+
+const ChangePasswordStore = FauxtonAPI.Store.extend({
+  initialize() {
+    this.reset();
+  },
+  reset() {
+    this._changePassword = "";
+    this._changePasswordConfirm = "";
+  },
+  getChangePassword() {
+    return this._changePassword;
+  },
+  getChangePasswordConfirm() {
+    return this._changePasswordConfirm;
+  },
+  setChangePassword(val) {
+    this._changePassword = val;
+  },
+  setChangePasswordConfirm(val) {
+    this._changePasswordConfirm = val;
+  },
+  dispatch(action) {
+    switch (action.type) {
+      case AUTH_CLEAR_CHANGE_PWD_FIELDS:
+        this.reset();
+        this.triggerChange();
+        break;
+
+      case AUTH_UPDATE_CHANGE_PWD_FIELD:
+        this.setChangePassword(action.value);
+        this.triggerChange();
+        break;
+
+      case AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD:
+        this.setChangePasswordConfirm(action.value);
+        this.triggerChange();
+        break;
+
+      default:
+        return;
+    }
+  }
+});
+
+const changePasswordStore = new ChangePasswordStore();
+
+changePasswordStore.dispatchToken = FauxtonAPI.dispatcher.register(
+  changePasswordStore.dispatch.bind(changePasswordStore)
+);
+
+export default changePasswordStore;
diff --git a/app/addons/auth/stores/CreateAdminSidebarStore.js b/app/addons/auth/stores/CreateAdminSidebarStore.js
new file mode 100644
index 0000000..220fbfc
--- /dev/null
+++ b/app/addons/auth/stores/CreateAdminSidebarStore.js
@@ -0,0 +1,35 @@
+import FauxtonAPI from "../../../core/api";
+import ActionTypes from "../actiontypes";
+
+const CreateAdminSidebarStore = FauxtonAPI.Store.extend({
+  initialize() {
+    this.reset();
+  },
+  reset() {
+    this._selectedPage = "changePassword";
+  },
+  getSelectedPage() {
+    return this._selectedPage;
+  },
+  setSelectedPage(val) {
+    this._selectedPage = val;
+  },
+  dispatch(action) {
+    switch (action.type) {
+      case ActionTypes.AUTH_SELECT_PAGE:
+        this.setSelectedPage(action.page);
+        this.triggerChange();
+        break;
+
+      default:
+        return;
+    }
+  }
+});
+
+const createAdminSidebarStore = new CreateAdminSidebarStore();
+createAdminSidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(
+  createAdminSidebarStore.dispatch.bind(createAdminSidebarStore)
+);
+
+export default createAdminSidebarStore;
diff --git a/app/addons/auth/stores/CreateAdminStore.js b/app/addons/auth/stores/CreateAdminStore.js
new file mode 100644
index 0000000..f005201
--- /dev/null
+++ b/app/addons/auth/stores/CreateAdminStore.js
@@ -0,0 +1,57 @@
+import FauxtonAPI from "../../../core/api";
+import ActionTypes from "../actiontypes";
+const {
+  AUTH_CLEAR_CREATE_ADMIN_FIELDS,
+  AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
+  AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD
+} = ActionTypes;
+
+const CreateAdminStore = FauxtonAPI.Store.extend({
+  initialize() {
+    this.reset();
+  },
+  reset() {
+    this._username = "";
+    this._password = "";
+  },
+  getUsername() {
+    return this._username;
+  },
+  getPassword() {
+    return this._password;
+  },
+  setUsername(val) {
+    this._username = val;
+  },
+  setPassword(val) {
+    this._password = val;
+  },
+  dispatch(action) {
+    switch (action.type) {
+      case AUTH_CLEAR_CREATE_ADMIN_FIELDS:
+        this.reset();
+        this.triggerChange();
+        break;
+
+      case AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD:
+        this.setUsername(action.value);
+        this.triggerChange();
+        break;
+
+      case AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD:
+        this.setPassword(action.value);
+        this.triggerChange();
+        break;
+
+      default:
+        return;
+    }
+  }
+});
+
+const createAdminStore = new CreateAdminStore();
+createAdminStore.dispatchToken = FauxtonAPI.dispatcher.register(
+  createAdminStore.dispatch.bind(createAdminStore)
+);
+
+export default createAdminStore;
diff --git a/app/addons/auth/stores/index.js b/app/addons/auth/stores/index.js
new file mode 100644
index 0000000..6a6ff7d
--- /dev/null
+++ b/app/addons/auth/stores/index.js
@@ -0,0 +1,13 @@
+// Not thrilled with this. The sole purpose of these next two stores is because the Create Admin + Change Password
+// forms need to clear after a successful post. Since those events occur in actions.js, we need a way to tell the
+// component to update + clear the fields. That's why all this code exists.
+
+import changePasswordStore from './ChangePassword';
+import createAdminStore from './CreateAdminStore';
+import createAdminSidebarStore from './CreateAdminSidebarStore.js';
+
+export default ({
+  changePasswordStore,
+  createAdminStore,
+  createAdminSidebarStore
+});
diff --git a/app/addons/auth/test/auth.componentsSpec.js b/app/addons/auth/test/auth.componentsSpec.react.jsx
similarity index 97%
rename from app/addons/auth/test/auth.componentsSpec.js
rename to app/addons/auth/test/auth.componentsSpec.react.jsx
index 3b5f1d8..c537b97 100644
--- a/app/addons/auth/test/auth.componentsSpec.js
+++ b/app/addons/auth/test/auth.componentsSpec.react.jsx
@@ -13,9 +13,9 @@ import FauxtonAPI from "../../../core/api";
 import React from "react";
 import ReactDOM from "react-dom";
 import utils from "../../../../test/mocha/testUtils";
-import Components from "../components";
-import Stores from "../stores";
-import Actions from "../actions";
+import * as Components from "../components";
+import * as Stores from "../stores";
+import * as Actions from "../actions";
 import TestUtils from "react-addons-test-utils";
 import { mount } from 'enzyme';
 import sinon from "sinon";
diff --git a/app/addons/documents/changes/components.react.jsx b/app/addons/documents/changes/components.react.jsx
new file mode 100644
index 0000000..db67ee3
--- /dev/null
+++ b/app/addons/documents/changes/components.react.jsx
@@ -0,0 +1,352 @@
+// 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.
+
+import FauxtonAPI from "../../../core/api";
+import React from "react";
+import ReactDOM from "react-dom";
+import Actions from "./actions";
+import Stores from "./stores";
+import Components from "../../fauxton/components.react";
+import ReactComponents from "../../components/react-components.react";
+import ReactCSSTransitionGroup from "react-addons-css-transition-group";
+import "../../../../assets/js/plugins/prettify";
+import uuid from 'uuid';
+
+const store = Stores.changesStore;
+const BadgeList = ReactComponents.BadgeList;
+const {Copy} = ReactComponents;
+
+class ChangesController extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = this.getStoreState();
+  }
+
+  getStoreState () {
+    return {
+      changes: store.getChanges(),
+      loaded: store.isLoaded(),
+      databaseName: store.getDatabaseName(),
+      isShowingSubset: store.isShowingSubset()
+    };
+  }
+
+  onChange () {
+    this.setState(this.getStoreState());
+  }
+
+  componentDidMount () {
+    store.on('change', this.onChange, this);
+  }
+
+  componentWillUnmount () {
+    store.off('change', this.onChange);
+  }
+
+  showingSubsetMsg () {
+    const { isShowingSubset, changes } = this.state;
+    let msg = '';
+    if (isShowingSubset) {
+      let numChanges = changes.length;
+      msg = <p className="changes-result-limit">Limiting results to latest <b>{numChanges}</b> changes.</p>;
+    }
+    return msg;
+  }
+
+  getRows () {
+    const { changes, loaded, databaseName } = this.state;
+    if (!changes.length && loaded) {
+      return (
+        <p className="no-doc-changes">
+          There are no document changes to display.
+        </p>
+      );
+    }
+
+    return changes.map((change, i) => {
+      return <ChangeRow change={change} key={i} databaseName={databaseName} />;
+    });
+  }
+
+  render () {
+    return (
+      <div>
+        <div className="js-changes-view">
+          {this.showingSubsetMsg()}
+          {this.getRows()}
+        </div>
+      </div>
+    );
+  }
+}
+
+
+class ChangesTabContent extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = this.getStoreState();
+  }
+
+  getStoreState () {
+    return {
+      filters: store.getFilters()
+    };
+  }
+
+  onChange () {
+    this.setState(this.getStoreState());
+  }
+
+  componentDidMount () {
+    store.on('change', this.onChange, this);
+  }
+
+  componentWillUnmount () {
+    store.off('change', this.onChange);
+  }
+
+  addFilter (newFilter) {
+    if (_.isEmpty(newFilter)) {
+      return;
+    }
+    Actions.addFilter(newFilter);
+  }
+
+  hasFilter (filter) {
+    return store.hasFilter(filter);
+  }
+
+  render () {
+    return (
+      <div className="changes-header">
+        <AddFilterForm tooltip={this.props.tooltip} filter={(label) => Actions.removeFilter(label)} addFilter={this.addFilter}
+          hasFilter={this.hasFilter} />
+        <BadgeList elements={this.state.filters} removeBadge={(label) => Actions.removeFilter(label)} />
+      </div>
+    );
+  }
+}
+
+
+class AddFilterForm extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      filter: '',
+      error: false
+    };
+    this.submitForm = this.submitForm.bind(this);
+  }
+
+  submitForm (e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    if (this.props.hasFilter(this.state.filter)) {
+      this.setState({ error: true });
+
+      // Yuck. This removes the class after the effect has completed so it can occur again. The
+      // other option is to use jQuery to add the flash. This seemed slightly less crumby
+      let component = this;
+      setTimeout(function () {
+        component.setState({ error: false });
+      }, 1000);
+    } else {
+      this.props.addFilter(this.state.filter);
+      this.setState({ filter: '', error: false });
+    }
+  }
+
+  componentDidMount () {
+    this.focusFilterField();
+  }
+
+  componentDidUpdate () {
+    this.focusFilterField();
+  }
+
+  focusFilterField () {
+    ReactDOM.findDOMNode(this.refs.addItem).focus();
+  }
+
+  inputClassNames () {
+    let className = 'js-changes-filter-field';
+    if (this.state.error) {
+      className += ' errorHighlight';
+    }
+    return className;
+  }
+
+  render () {
+    return (
+      <form className="form-inline js-filter-form" onSubmit={this.submitForm}>
+        <fieldset>
+          <i className="fonticon-filter" />
+          <input type="text" ref="addItem" className={this.inputClassNames()} placeholder="Sequence or ID"
+                 onChange={(e) => this.setState({ filter: e.target.value })} value={this.state.filter} />
+          <button type="submit" className="btn btn-secondary">Filter</button>
+          <div className="help-block">
+            <FilterTooltip tooltip={this.props.tooltip} />
+          </div>
+        </fieldset>
+      </form>
+    );
+  }
+}
+AddFilterForm.PropTypes = {
+  addFilter: React.PropTypes.func.isRequired,
+  hasFilter: React.PropTypes.func.isRequired,
+  tooltips: React.PropTypes.string
+};
+AddFilterForm.defaultProps = {
+  tooltip: ''
+};
+
+
+class FilterTooltip extends React.Component {
+  componentDidMount () {
+    if (this.props.tooltip) {
+      $(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip();
+    }
+  }
+
+  render () {
+    if (!this.props.tooltip) {
+      return null;
+    }
+    return (
+      <i ref="tooltip" className="icon icon-question-sign js-filter-tooltip" data-toggle="tooltip"
+         data-original-title={this.props.tooltip} />
+    );
+  }
+}
+
+
+class ChangeRow extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      codeVisible: false
+    };
+  }
+
+  toggleJSON (e) {
+    e.preventDefault();
+    this.setState({ codeVisible: !this.state.codeVisible });
+  }
+
+  getChangesCode () {
+    return (this.state.codeVisible) ? <Components.CodeFormat key="changesCodeSection" code={this.getChangeCode()} /> : null;
+  }
+
+  getChangeCode () {
+    return {
+      changes: this.props.change.changes,
+      doc: this.props.change.doc
+    };
+  }
+
+  showCopiedMessage (target) {
+    let msg = 'The document ID has been copied to your clipboard.';
+    if (target === 'seq') {
+      msg = 'The document seq number has been copied to your clipboard.';
+    }
+    FauxtonAPI.addNotification({
+      msg: msg,
+      type: 'info',
+      clear: true
+    });
+  }
+
+  render () {
+    const { codeVisible } = this.state;
+    const { change, databaseName } = this.props;
+    const wrapperClass = 'change-wrapper' + (change.isNew ? ' new-change-row' : '');
+
+    return (
+      <div className={wrapperClass}>
+        <div className="change-box" data-id={change.id}>
+          <div className="row-fluid">
+            <div className="span2">seq</div>
+            <div className="span8 change-sequence">{change.seq}</div>
+            <div className="span2 text-right">
+              <Copy
+                uniqueKey={uuid.v4()}
+                text={change.seq.toString()}
+                onClipboardClick={() => this.showCopiedMessage('seq')} />
+            </div>
+          </div>
+
+          <div className="row-fluid">
+            <div className="span2">id</div>
+            <div className="span8">
+              <ChangeID id={change.id} deleted={change.deleted} databaseName={databaseName} />
+            </div>
+            <div className="span2 text-right">
+              <Copy
+                uniqueKey={uuid.v4()}
+                text={change.id}
+                onClipboardClick={() => this.showCopiedMessage('id')} />
+            </div>
+          </div>
+
+          <div className="row-fluid">
+            <div className="span2">changes</div>
+            <div className="span10">
+              <button type="button" className='btn btn-small btn-secondary' onClick={this.toggleJSON.bind(this)}>
+                {codeVisible ? 'Close JSON' : 'View JSON'}
+              </button>
+            </div>
+          </div>
+
+          <ReactCSSTransitionGroup transitionName="toggle-changes-code" component="div" className="changesCodeSectionWrapper"
+            transitionEnterTimeout={500} transitionLeaveTimeout={300}>
+            {this.getChangesCode()}
+          </ReactCSSTransitionGroup>
+
+          <div className="row-fluid">
+            <div className="span2">deleted</div>
+            <div className="span10">{change.deleted ? 'True' : 'False'}</div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+ChangeRow.PropTypes = {
+  change: React.PropTypes.object,
+  databaseName: React.PropTypes.string.isRequired
+};
+
+
+class ChangeID extends React.Component {
+  render () {
+    const { deleted, id, databaseName } = this.props;
+    if (deleted) {
+      return (
+        <span className="js-doc-id">{id}</span>
+      );
+    }
+    const link = '#' + FauxtonAPI.urls('document', 'app', databaseName, id);
+    return (
+      <a href={link} className="js-doc-link">{id}</a>
+    );
+  }
+}
+
+
+export default {
+  ChangesController,
+  ChangesTabContent,
+  ChangeRow,
+  ChangeID
+};
diff --git a/app/addons/replication/base.js b/app/addons/replication/base.js
index 02efe80..0392c67 100644
--- a/app/addons/replication/base.js
+++ b/app/addons/replication/base.js
@@ -17,12 +17,8 @@ import Actions from './actions';
 
 replication.initialize = function () {
   FauxtonAPI.addHeaderLink({ title: 'Replication', href: '#/replication', icon: 'fonticon-replicate' });
-  FauxtonAPI.session.on('authenticated', () => {
-    if (!FauxtonAPI.session.isLoggedIn()) {
-      //don't check until user is logged in
-      return;
-    }
-
+  FauxtonAPI.session.isAuthenticated().then(() => {
+    console.log('AUAUAU', FauxtonAPI.session.isLoggedIn());
     Actions.checkForNewApi();
   });
 };
diff --git a/app/app.js b/app/app.js
index cf9be21..29b5730 100644
--- a/app/app.js
+++ b/app/app.js
@@ -16,7 +16,7 @@ import "bootstrap";
 import Helpers from "./helpers";
 import Utils from "./core/utils";
 import FauxtonAPI from "./core/api";
-import Couchdb from "./core/couchdbSession";
+import Session from "./core/session";
 import "../assets/less/fauxton.less";
 
 // Make sure we have a console.log
@@ -46,10 +46,8 @@ Object.assign(app, {
   helpers: Helpers
 });
 
-FauxtonAPI.setSession(new Couchdb.Session());
+FauxtonAPI.setSession(new Session());
 
-// Define your master router on the application namespace and trigger all
-// navigation from this instance.
 FauxtonAPI.config({
   // I haven't wrapped these dispatch methods in a action
   // because I don't want to require fauxton/actions in this method.
diff --git a/app/core/auth.js b/app/core/auth.js
index e296c39..a77342b 100644
--- a/app/core/auth.js
+++ b/app/core/auth.js
@@ -11,50 +11,30 @@
 // the License.
 
 import FauxtonAPI from "./base";
-import Backbone from "backbone";
-import _ from "lodash";
 
-// This is not exposed externally as it should not need to be accessed or overridden
-var Auth = function (options) {
-  this._options = options;
-  this.initialize.apply(this, arguments);
-};
-
-// Piggy-back on Backbone's self-propagating extend function,
-Auth.extend = Backbone.Model.extend;
-
-_.extend(Auth.prototype, Backbone.Events, {
-  authDeniedCb: function () {},
-
-  initialize: function () {
-  },
-
-  authHandlerCb : function () {
-    var deferred = $.Deferred();
-    deferred.resolve();
-    return deferred;
-  },
-
-  registerAuth: function (authHandlerCb) {
-    this.authHandlerCb = authHandlerCb;
-  },
-
-  registerAuthDenied: function (authDeniedCb) {
-    this.authDeniedCb = authDeniedCb;
-  },
+function authenticate(session, roles) {
+  if (session.isAdminParty()) {
+    return true;
+  } else if (session.matchesRoles(roles)) {
+    return true;
+  } else {
+    throw new Error('Unable to authenticate');
+  }
+}
 
-  checkAccess: function (roles) {
-    var requiredRoles = roles || [],
-    that = this;
+function authenticationDenied() {
+  let url = window.location.hash
+    .replace('#', '')
+    .replace('login?urlback=', '');
 
-    if (!FauxtonAPI.session) {
-      throw new Error("Fauxton.session is not configured.");
-    }
+  FauxtonAPI.navigate(`/login?urlback=${url}`, { replace: true });
+};
 
-    return FauxtonAPI.session.fetchUser().then(function () {
-      return FauxtonAPI.when(that.authHandlerCb(FauxtonAPI.session, requiredRoles));
-    });
+export default class {
+  checkAccess(roles = []) {
+    return FauxtonAPI.session
+      .fetchUser()
+      .then(() => authenticate(FauxtonAPI.session, roles))
+      .catch(authenticationDenied);
   }
-});
-
-export default Auth;
+}
diff --git a/app/core/base.js b/app/core/base.js
index 0f8cfeb..5ab6ced 100644
--- a/app/core/base.js
+++ b/app/core/base.js
@@ -155,7 +155,7 @@ FauxtonAPI.extensions = extensions;
 
 FauxtonAPI.setSession = function (newSession) {
   FauxtonAPI.session = newSession;
-  return FauxtonAPI.session.fetchUser();
+  //return FauxtonAPI.session.fetchUser();
 };
 
 FauxtonAPI.reducers = {};
diff --git a/app/core/couchdb/admin.js b/app/core/couchdb/admin.js
new file mode 100644
index 0000000..9e800a0
--- /dev/null
+++ b/app/core/couchdb/admin.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.
+
+import app from './../../app';
+import { json } from '../http';
+
+export function create({name, password, node}) {
+  return json(`${app.host}/_node/${node}/_config/admins/${name}`, {
+    method: "PUT",
+    body: JSON.stringify(password)
+  });
+}
diff --git a/app/core/couchdb/index.js b/app/core/couchdb/index.js
new file mode 100644
index 0000000..fdba344
--- /dev/null
+++ b/app/core/couchdb/index.js
@@ -0,0 +1,19 @@
+// 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.
+
+import * as session from './session';
+import * as admin from './admin';
+
+export default ({
+  session,
+  admin
+});
diff --git a/app/core/couchdb/session.js b/app/core/couchdb/session.js
new file mode 100644
index 0000000..1fac09d
--- /dev/null
+++ b/app/core/couchdb/session.js
@@ -0,0 +1,32 @@
+// 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.
+
+import app from "./../../app";
+import { json } from "../http";
+
+export function get() {
+  return json(app.host + "/_session");
+}
+
+export function create(body) {
+  return json(app.host + "/_session", {
+    method: "POST",
+    body: JSON.stringify(body)
+  });
+}
+
+export function remove() {
+  return json(app.host + "/_session", {
+    method: "DELETE",
+    body: JSON.stringify({ username: "_", password: "_" })
+  });
+}
diff --git a/app/core/http.js b/app/core/http.js
new file mode 100644
index 0000000..f66524d
--- /dev/null
+++ b/app/core/http.js
@@ -0,0 +1,27 @@
+// 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.
+
+import { defaultsDeep } from "lodash";
+
+export const json = (url, opts = {}) => fetch(
+  url,
+  defaultsDeep(
+    {
+      credentials: "include",
+      headers: {
+        accept: "application/json",
+        "Content-Type": "application/json"
+      }
+    },
+    opts
+  )
+).then(res => res.ok ? res.json() : { error: res.statusText });
diff --git a/app/core/session.js b/app/core/session.js
new file mode 100644
index 0000000..8867792
--- /dev/null
+++ b/app/core/session.js
@@ -0,0 +1,166 @@
+// 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.
+import couchdb from "./couchdb";
+import { uniqueId, each } from "lodash";
+import Promise from 'Bluebird';
+
+const authMessages = {
+  missingCredentials: "Username or password cannot be blank.",
+  loggedIn: "You have been logged in.",
+  adminCreated: "CouchDB admin created",
+  changePassword: "Your password has been updated.",
+  adminCreationFailedPrefix: "Could not create admin.",
+  passwordsNotMatching: "Passwords do not match."
+};
+
+function isAdmin(roles = []) {
+  return roles.includes("_admin");
+}
+
+function validate(...predicates) {
+  return predicates.every(isTrue => isTrue);
+}
+
+export default class {
+
+  constructor() {
+    this._user = {
+      roles: []
+    };
+    this._onChange = {};
+    this.messages = authMessages;
+    this._authenticatedPromise = new Promise(resolve => {
+      this._authenticateResolve = resolve;
+    });
+  }
+  getUserFromSession() {
+    return couchdb.session.get().then(({ userCtx }) => {
+      return userCtx;
+    });
+  }
+
+  isAuthenticated () {
+    return this._authenticatedPromise;
+  }
+
+  setUser(user) {
+    if (this._user.name !== user.name) {
+      this._user = {
+        name: user.name,
+        roles: user.roles,
+        isAdmin: isAdmin(user.roles)
+      };
+      each(this._onChange, fn => fn(this._user));
+    }
+    return this._user;
+  }
+
+  user() {
+    return this._user;
+  }
+
+  fetchUser() {
+    return this.getUserFromSession().then(userCtx => {
+      this.setUser(userCtx);
+      console.log('HAve user', this.isLoggedIn());
+      if (this.isLoggedIn()) {
+        this._authenticateResolve(this.user());
+      }
+    });
+  }
+
+  isAdminParty() {
+    return !this._user.name && this._user.isAdmin;
+  }
+
+  isLoggedIn() {
+    return !_.isNull(this._user.name);
+  }
+
+  userRoles() {
+    const user = this._user;
+
+    console.log('UU', this._user, this.isLoggedIn());
+    //if (user && user.roles) {
+    if (this.isLoggedIn()) {
+      if (user.roles.indexOf("fx_loggedIn") === -1) {
+        user.roles.push("fx_loggedIn");
+      }
+      return user.roles;
+    }
+    return [];
+  }
+
+  onChange(fn) {
+    let uuid = uniqueId();
+    this._onChange[uuid] = fn;
+    return () => {
+      delete this._onChange[uuid];
+    };
+  }
+
+  matchesRoles(roles = []) {
+    const numberMatchingRoles = _.intersection(this.userRoles(), roles).length;
+    return numberMatchingRoles > 0;
+  }
+
+  validateUser(username, password, msg) {
+    const isValid = validate(!_.isEmpty(username), !_.isEmpty(password));
+    return isValid ? Promise.resolve() : Promise.reject(msg);
+  }
+
+  validatePasswords(password, password_confirm, msg) {
+    const isValid = validate(
+      _.isEmpty(password),
+      _.isEmpty(password_confirm),
+      password !== password_confirm
+    );
+    return isValid ? Promise.resolve() : Promise.reject(msg);
+  }
+
+  createAdmin(username, password, login, node) {
+    return this
+      .validateUser(username, password, this.messages.missingCredentials)
+      .then(() => couchdb.admin.create({ name: this._user.name, password, node }))
+      .then(
+        () =>
+          login
+            ? this.login(username, password)
+            : this.fetchUser({ forceFetch: true })
+      );
+  }
+
+  login(username, password) {
+    return this
+      .validateUser(username, password, authMessages.missingCredentials)
+      .then(() => couchdb.session.create({ name: username, password: password }))
+      .then((res) => {
+        if (res.error) throw new Error(res.error);
+      })
+      .then(() => this.fetchUser());
+  }
+
+  logout() {
+    return couchdb.session.remove().then(() => this.fetchUser());
+  }
+
+  changePassword(password, confirmedPassword, node) {
+    return this
+      .validatePasswords(
+        password,
+        confirmedPassword,
+        authMessages.passwordsNotMatching
+      )
+      .then(() => couchdb.admin.create({ name: this._user.name, password, node }))
+      .then(() => this.login(this._user.name, password));
+  }
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.