Start implementing backends and authentication

This commit is contained in:
Mathias Biilmann Christensen 2016-02-25 12:31:21 -08:00
parent c60d8ba706
commit 67cdd92bfb
13 changed files with 247 additions and 15 deletions

38
src/actions/auth.js Normal file
View File

@ -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)));
};
}

View File

@ -1,27 +1,25 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
export const CONFIG = { export const CONFIG_REQUEST = 'CONFIG_REQUEST';
REQUEST: 'REQUEST', export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
SUCCESS: 'SUCCESS', export const CONFIG_FAILURE = 'CONFIG_FAILURE';
FAILURE: 'FAILURE'
};
export function configLoaded(config) { export function configLoaded(config) {
return { return {
type: CONFIG.SUCCESS, type: CONFIG_SUCCESS,
payload: config payload: config
}; };
} }
export function configLoading() { export function configLoading() {
return { return {
type: CONFIG.REQUEST type: CONFIG_REQUEST
}; };
} }
export function configFailed(err) { export function configFailed(err) {
return { return {
type: CONFIG.FAILURE, type: CONFIG_FAILURE,
error: 'Error loading config', error: 'Error loading config',
payload: err payload: err
}; };

49
src/backends/backend.js Normal file
View File

@ -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);
}
};
})();

View File

@ -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 <div>
<p>
<label>Your name or email: <input type='text' onChange={this.handleEmailChange}/></label>
</p>
<p>
<button onClick={this.handleLogin}>Login</button>
</p>
</div>;
}
}

View File

@ -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});
}
}

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loadConfig } from '../actions/config'; import { loadConfig } from '../actions/config';
import { loginUser } from '../actions/auth';
import { currentBackend } from '../backends/backend';
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
@ -11,6 +13,10 @@ class App extends React.Component {
this.props.dispatch(loadConfig()); this.props.dispatch(loadConfig());
} }
componentWillReceiveProps(nextProps) {
//this.props.dispatch(loadBackend());
}
configError(config) { configError(config) {
return <div> return <div>
<h1>Error loading the CMS configuration</h1> <h1>Error loading the CMS configuration</h1>
@ -28,8 +34,29 @@ class App extends React.Component {
</div>; </div>;
} }
handleLogin(credentials) {
this.props.dispatch(loginUser(credentials));
}
authenticating() {
const { auth } = this.props;
const backend = currentBackend(this.props.config);
if (backend == null) {
return <div><h1>Waiting for backend...</h1></div>;
}
return <div>
{React.createElement(backend.authComponent(), {
onLogin: this.handleLogin.bind(this),
error: auth && auth.get('error'),
isFetching: auth && auth.get('isFetching')
})}
</div>;
}
render() { render() {
const { config, children } = this.props; const { user, config, children } = this.props;
if (config === null) { if (config === null) {
return null; return null;
@ -43,6 +70,10 @@ class App extends React.Component {
return this.configLoading(); return this.configLoading();
} }
if (user == null) {
return this.authenticating();
}
return ( return (
<div>{children}</div> <div>{children}</div>
); );
@ -50,7 +81,11 @@ class App extends React.Component {
} }
function mapStateToProps(state) { function mapStateToProps(state) {
const { auth } = state;
return { return {
auth: auth,
user: auth && auth.get('user'),
config: state.config config: state.config
}; };
} }

View File

@ -0,0 +1,5 @@
import React from 'react';
export default () => (
<h2>Not Found</h2>
);

15
src/reducers/auth.js Normal file
View File

@ -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;
}
}

View File

@ -1,9 +1,9 @@
import Immutable from 'immutable'; import Immutable from 'immutable';
import { CONFIG } from '../actions/config'; import { CONFIG_SUCCESS } from '../actions/config';
export function collections(state = null, action) { export function collections(state = null, action) {
switch (action.type) { switch (action.type) {
case CONFIG.SUCCESS: case CONFIG_SUCCESS:
const collections = action.payload && action.payload.collections; const collections = action.payload && action.payload.collections;
return Immutable.OrderedMap().withMutations((map) => { return Immutable.OrderedMap().withMutations((map) => {
(collections || []).forEach(function(collection) { (collections || []).forEach(function(collection) {

View File

@ -1,13 +1,13 @@
import Immutable from 'immutable'; 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) { export function config(state = null, action) {
switch (action.type) { switch (action.type) {
case CONFIG.REQUEST: case CONFIG_REQUEST:
return Immutable.Map({isFetching: true}); return Immutable.Map({isFetching: true});
case CONFIG.SUCCESS: case CONFIG_SUCCESS:
return Immutable.fromJS(action.payload); return Immutable.fromJS(action.payload);
case CONFIG.FAILURE: case CONFIG_FAILURE:
return Immutable.Map({error: action.payload.toString()}); return Immutable.Map({error: action.payload.toString()});
default: default:
return state; return state;

View File

@ -2,11 +2,13 @@ import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router'; import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App'; import App from '../containers/App';
import DashboardPage from '../containers/DashboardPage'; import DashboardPage from '../containers/DashboardPage';
import NotFoundPage from '../containers/NotFoundPage';
export default () => ( export default () => (
<Router history={browserHistory}> <Router history={browserHistory}>
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRoute component={DashboardPage}/> <IndexRoute component={DashboardPage}/>
<Route path="*" component={NotFoundPage}/>
</Route> </Route>
</Router> </Router>
); );

View File

@ -2,10 +2,12 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import { syncHistory, routeReducer } from 'react-router-redux'; import { syncHistory, routeReducer } from 'react-router-redux';
import { auth } from '../reducers/auth';
import { config } from '../reducers/config'; import { config } from '../reducers/config';
import { collections } from '../reducers/collections'; import { collections } from '../reducers/collections';
const reducer = combineReducers({ const reducer = combineReducers({
auth,
config, config,
collections, collections,
router: routeReducer router: routeReducer

View File

@ -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'
})
);
});
});