Start implementing backends and authentication
This commit is contained in:
parent
c60d8ba706
commit
67cdd92bfb
38
src/actions/auth.js
Normal file
38
src/actions/auth.js
Normal 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)));
|
||||||
|
};
|
||||||
|
}
|
@ -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
49
src/backends/backend.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
33
src/backends/test-repo/AuthenticationPage.js
Normal file
33
src/backends/test-repo/AuthenticationPage.js
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
15
src/backends/test-repo/implementation.js
Normal file
15
src/backends/test-repo/implementation.js
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
5
src/containers/NotFoundPage.js
Normal file
5
src/containers/NotFoundPage.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<h2>Not Found</h2>
|
||||||
|
);
|
15
src/reducers/auth.js
Normal file
15
src/reducers/auth.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
40
test/reducers/auth.spec.js
Normal file
40
test/reducers/auth.spec.js
Normal 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'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user