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>.