diff --git a/src/actions/auth.js b/src/actions/auth.js
new file mode 100644
index 00000000..4adff757
--- /dev/null
+++ b/src/actions/auth.js
@@ -0,0 +1,38 @@
+import { currentBackend } from '../backends/backend';
+
+export const AUTH_REQUEST = 'AUTH_REQUEST';
+export const AUTH_SUCCESS = 'AUTH_SUCCESS';
+export const AUTH_FAILURE = 'AUTH_FAILURE';
+
+export function authenticating() {
+ return {
+ type: AUTH_REQUEST
+ };
+}
+
+export function authenticate(userData) {
+ return {
+ type: AUTH_SUCCESS,
+ payload: userData
+ };
+}
+
+export function authError(error) {
+ return {
+ type: AUTH_FAILURE,
+ error: 'Failed to authenticate',
+ payload: error,
+ };
+}
+
+export function loginUser(credentials) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const backend = currentBackend(state.config);
+
+ dispatch(authenticating());
+ backend.authenticate(credentials)
+ .then((user) => dispatch(authenticate(user)))
+ .catch((err) => dispatch(authError(err)));
+ };
+}
diff --git a/src/actions/config.js b/src/actions/config.js
index 85141672..1bb3a346 100644
--- a/src/actions/config.js
+++ b/src/actions/config.js
@@ -1,27 +1,25 @@
import yaml from 'js-yaml';
-export const CONFIG = {
- REQUEST: 'REQUEST',
- SUCCESS: 'SUCCESS',
- FAILURE: 'FAILURE'
-};
+export const CONFIG_REQUEST = 'CONFIG_REQUEST';
+export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
+export const CONFIG_FAILURE = 'CONFIG_FAILURE';
export function configLoaded(config) {
return {
- type: CONFIG.SUCCESS,
+ type: CONFIG_SUCCESS,
payload: config
};
}
export function configLoading() {
return {
- type: CONFIG.REQUEST
+ type: CONFIG_REQUEST
};
}
export function configFailed(err) {
return {
- type: CONFIG.FAILURE,
+ type: CONFIG_FAILURE,
error: 'Error loading config',
payload: err
};
diff --git a/src/backends/backend.js b/src/backends/backend.js
new file mode 100644
index 00000000..073b96f6
--- /dev/null
+++ b/src/backends/backend.js
@@ -0,0 +1,49 @@
+import TestRepoBackend from './test-repo/Implementation';
+
+export function resolveBackend(config) {
+ const name = config.getIn(['backend', 'name']);
+ if (name == null) {
+ throw 'No backend defined in configuration';
+ }
+
+ switch (name) {
+ case 'test-repo':
+ return new Backend(new TestRepoBackend(config));
+ default:
+ throw `Backend not found: ${name}`;
+ }
+}
+
+class Backend {
+ constructor(implementation, authStore = null) {
+ this.implementation = implementation;
+ this.authStore = authStore;
+ if (this.implementation == null) {
+ throw 'Cannot instantiate a Backend with no implementation';
+ }
+ }
+
+ currentUser() {
+ if (this.user) { return this.user; }
+ return this.authStore && this.authStore.retrieve();
+ }
+
+ authComponent() {
+ return this.implementation.authComponent();
+ }
+
+ authenticate(state) {
+ return this.implementation.authenticate(state);
+ }
+}
+
+export const currentBackend = (function() {
+ let backend = null;
+
+ return (config) => {
+ if (backend) { return backend; }
+ if (config.get('backend')) {
+ return backend = resolveBackend(config);
+ }
+ };
+})();
diff --git a/src/backends/test-repo/AuthenticationPage.js b/src/backends/test-repo/AuthenticationPage.js
new file mode 100644
index 00000000..6e46d546
--- /dev/null
+++ b/src/backends/test-repo/AuthenticationPage.js
@@ -0,0 +1,33 @@
+import React from 'react';
+
+export default class AuthenticationPage extends React.Component {
+ static propTypes = {
+ onLogin: React.PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {email: ''};
+ this.handleLogin = this.handleLogin.bind(this);
+ this.handleEmailChange = this.handleEmailChange.bind(this);
+ }
+
+ handleLogin() {
+ this.props.onLogin(this.state);
+ }
+
+ handleEmailChange(e) {
+ this.setState({email: e.target.value});
+ }
+
+ render() {
+ return
+
+
+
+
+
+
+
;
+ }
+}
diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js
new file mode 100644
index 00000000..f029d873
--- /dev/null
+++ b/src/backends/test-repo/implementation.js
@@ -0,0 +1,15 @@
+import AuthenticationPage from './AuthenticationPage';
+
+export default class TestRepo {
+ constructor(config) {
+ this.config = config;
+ }
+
+ authComponent() {
+ return AuthenticationPage;
+ }
+
+ authenticate(state) {
+ return Promise.resolve({email: state.email});
+ }
+}
diff --git a/src/containers/App.js b/src/containers/App.js
index 2f001964..629169e0 100644
--- a/src/containers/App.js
+++ b/src/containers/App.js
@@ -1,6 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { loadConfig } from '../actions/config';
+import { loginUser } from '../actions/auth';
+import { currentBackend } from '../backends/backend';
class App extends React.Component {
constructor(props) {
@@ -11,6 +13,10 @@ class App extends React.Component {
this.props.dispatch(loadConfig());
}
+ componentWillReceiveProps(nextProps) {
+ //this.props.dispatch(loadBackend());
+ }
+
configError(config) {
return
Error loading the CMS configuration
@@ -28,8 +34,29 @@ class App extends React.Component {
;
}
+ handleLogin(credentials) {
+ this.props.dispatch(loginUser(credentials));
+ }
+
+ authenticating() {
+ const { auth } = this.props;
+ const backend = currentBackend(this.props.config);
+
+ if (backend == null) {
+ return Waiting for backend...
;
+ }
+
+ return
+ {React.createElement(backend.authComponent(), {
+ onLogin: this.handleLogin.bind(this),
+ error: auth && auth.get('error'),
+ isFetching: auth && auth.get('isFetching')
+ })}
+
;
+ }
+
render() {
- const { config, children } = this.props;
+ const { user, config, children } = this.props;
if (config === null) {
return null;
@@ -43,6 +70,10 @@ class App extends React.Component {
return this.configLoading();
}
+ if (user == null) {
+ return this.authenticating();
+ }
+
return (
{children}
);
@@ -50,7 +81,11 @@ class App extends React.Component {
}
function mapStateToProps(state) {
+ const { auth } = state;
+
return {
+ auth: auth,
+ user: auth && auth.get('user'),
config: state.config
};
}
diff --git a/src/containers/NotFoundPage.js b/src/containers/NotFoundPage.js
new file mode 100644
index 00000000..1361270a
--- /dev/null
+++ b/src/containers/NotFoundPage.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default () => (
+ Not Found
+);
diff --git a/src/reducers/auth.js b/src/reducers/auth.js
new file mode 100644
index 00000000..4a4573cf
--- /dev/null
+++ b/src/reducers/auth.js
@@ -0,0 +1,15 @@
+import Immutable from 'immutable';
+import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from '../actions/auth';
+
+export function auth(state = null, action) {
+ switch (action.type) {
+ case AUTH_REQUEST:
+ return Immutable.Map({isFetching: true});
+ case AUTH_SUCCESS:
+ return Immutable.fromJS({user: action.payload});
+ case AUTH_FAILURE:
+ return Immutable.Map({error: action.payload.toString()});
+ default:
+ return state;
+ }
+}
diff --git a/src/reducers/collections.js b/src/reducers/collections.js
index cee6d770..5451dbbb 100644
--- a/src/reducers/collections.js
+++ b/src/reducers/collections.js
@@ -1,9 +1,9 @@
import Immutable from 'immutable';
-import { CONFIG } from '../actions/config';
+import { CONFIG_SUCCESS } from '../actions/config';
export function collections(state = null, action) {
switch (action.type) {
- case CONFIG.SUCCESS:
+ case CONFIG_SUCCESS:
const collections = action.payload && action.payload.collections;
return Immutable.OrderedMap().withMutations((map) => {
(collections || []).forEach(function(collection) {
diff --git a/src/reducers/config.js b/src/reducers/config.js
index 740c7db9..c343001a 100644
--- a/src/reducers/config.js
+++ b/src/reducers/config.js
@@ -1,13 +1,13 @@
import Immutable from 'immutable';
-import { CONFIG } from '../actions/config';
+import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
export function config(state = null, action) {
switch (action.type) {
- case CONFIG.REQUEST:
+ case CONFIG_REQUEST:
return Immutable.Map({isFetching: true});
- case CONFIG.SUCCESS:
+ case CONFIG_SUCCESS:
return Immutable.fromJS(action.payload);
- case CONFIG.FAILURE:
+ case CONFIG_FAILURE:
return Immutable.Map({error: action.payload.toString()});
default:
return state;
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 340c47cf..98eed6d1 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -2,11 +2,13 @@ import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App';
import DashboardPage from '../containers/DashboardPage';
+import NotFoundPage from '../containers/NotFoundPage';
export default () => (
+
);
diff --git a/src/store/configureStore.js b/src/store/configureStore.js
index ffb31c87..92ea91d3 100644
--- a/src/store/configureStore.js
+++ b/src/store/configureStore.js
@@ -2,10 +2,12 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { browserHistory } from 'react-router';
import { syncHistory, routeReducer } from 'react-router-redux';
+import { auth } from '../reducers/auth';
import { config } from '../reducers/config';
import { collections } from '../reducers/collections';
const reducer = combineReducers({
+ auth,
config,
collections,
router: routeReducer
diff --git a/test/reducers/auth.spec.js b/test/reducers/auth.spec.js
new file mode 100644
index 00000000..1d766f53
--- /dev/null
+++ b/test/reducers/auth.spec.js
@@ -0,0 +1,40 @@
+import expect from 'expect';
+import Immutable from 'immutable';
+import { authenticating, authenticate, authError } from '../../src/actions/auth';
+import { auth } from '../../src/reducers/auth';
+
+describe('auth', () => {
+ it('should handle an empty state', () => {
+ expect(
+ auth(undefined, {})
+ ).toEqual(
+ null
+ );
+ });
+
+ it('should handle an authentication request', () => {
+ expect(
+ auth(undefined, authenticating())
+ ).toEqual(
+ Immutable.Map({isFetching: true})
+ );
+ });
+
+ it('should handle authentication', () => {
+ expect(
+ auth(undefined, authenticate({email: 'joe@example.com'}))
+ ).toEqual(
+ Immutable.fromJS({user: {email: 'joe@example.com'}})
+ );
+ });
+
+ it('should handle an authentication error', () => {
+ expect(
+ auth(undefined, authError(new Error('Bad credentials')))
+ ).toEqual(
+ Immutable.Map({
+ error: 'Error: Bad credentials'
+ })
+ );
+ });
+});