Merge pull request #6 from jorilallo/jori-mobx-migration

Migrated user/auth from redux to mobx
This commit is contained in:
Jori Lallo
2016-06-04 15:58:17 -07:00
13 changed files with 129 additions and 194 deletions

View File

@ -1,30 +0,0 @@
import makeActionCreator from '../utils/actions';
import { replace } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import auth from 'utils/auth';
export const SLACK_AUTH_PENDING = 'SLACK_AUTH_PENDING';
export const SLACK_AUTH_SUCCESS = 'SLACK_AUTH_SUCCESS';
export const SLACK_AUTH_FAILURE = 'SLACK_AUTH_FAILURE';
const slackAuthPending = makeActionCreator(SLACK_AUTH_PENDING);
const slackAuthSuccess = makeActionCreator(SLACK_AUTH_SUCCESS, 'user', 'team');
const slackAuthFailure = makeActionCreator(SLACK_AUTH_FAILURE, 'error');
export function slackAuthAsync(code) {
return (dispatch) => {
dispatch(slackAuthPending());
client.post('/auth.slack', {
code: code,
})
.then(data => {
auth.setToken(data.data.accessToken);
dispatch(slackAuthSuccess(data.data.user, data.data.team));
dispatch(replace('/dashboard'));
})
.catch((err) => {
dispatch(push('/error'));
})
};
};

View File

@ -1,15 +0,0 @@
import { push } from 'react-router-redux';
import auth from 'utils/auth';
import makeActionCreator from '../utils/actions';
export const UPDATE_USER = 'UPDATE_USER';
export const updateUser = makeActionCreator(UPDATE_USER, 'user');
export function logoutUser() {
return (dispatch) => {
auth.logout();
dispatch(push('/'));
};
};

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import Link from 'react-router/lib/Link'; import Link from 'react-router/lib/Link';
import { bindActionCreators } from 'redux'; import { observe } from 'mobx';
import { logoutUser } from 'actions/UserActions';
import store from 'stores/UserStore';
import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import Flex from 'components/Flex'; import Flex from 'components/Flex';
@ -21,10 +21,6 @@ class Layout extends React.Component {
loading: React.PropTypes.bool, loading: React.PropTypes.bool,
} }
onLogout = () => {
this.props.logoutUser();
}
render() { render() {
return ( return (
<div className={ styles.container }> <div className={ styles.container }>
@ -33,7 +29,7 @@ class Layout extends React.Component {
) : null } ) : null }
<div className={ cx(styles.header, { fixed: this.props.fixed }) }> <div className={ cx(styles.header, { fixed: this.props.fixed }) }>
<div className={ styles.teamName }> <div className={ styles.teamName }>
<Link to="/">{ this.props.teamName }</Link> <Link to="/">{ store.team.name }</Link>
</div> </div>
<Flex align="center" className={ styles.title }> <Flex align="center" className={ styles.title }>
{ this.props.title } { this.props.title }
@ -47,10 +43,10 @@ class Layout extends React.Component {
<Avatar <Avatar
circle circle
size={24} size={24}
src={ this.props.avatarUrl } src={ store.user.avatarUrl }
/> />
}> }>
<MenuItem onClick={ this.onLogout }>Logout</MenuItem> <MenuItem onClick={ store.logout }>Logout</MenuItem>
</DropdownMenu> </DropdownMenu>
</Flex> </Flex>
</div> </div>
@ -63,20 +59,4 @@ class Layout extends React.Component {
} }
} }
const mapStateToProps = (state) => { export default Layout;
return {
teamName: state.team ? state.team.name : null,
avatarUrl: state.user ? state.user.avatarUrl : null,
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
logoutUser,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Layout);

View File

@ -1,16 +1,14 @@
import React from 'react'; import React from 'react';
import { observe } from 'mobx'
import store from 'stores/UserStore';
import styles from './SlackAuthLink.scss'; import styles from './SlackAuthLink.scss';
export default class SlackAuthLink extends React.Component { class SlackAuthLink extends React.Component {
static propTypes = { static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string), scopes: React.PropTypes.arrayOf(React.PropTypes.string),
} }
state = {
oauthState: Math.random().toString(36).substring(7),
}
static defaultProps = { static defaultProps = {
scopes: [ scopes: [
'identity.email', 'identity.email',
@ -20,10 +18,6 @@ export default class SlackAuthLink extends React.Component {
] ]
} }
componentDidMount = () => {
localStorage.oauthState = this.state.oauthState;
}
slackUrl = () => { slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize'; const baseUrl = 'https://slack.com/oauth/authorize';
const params = { const params = {
@ -32,7 +26,7 @@ export default class SlackAuthLink extends React.Component {
redirect_uri: __DEV__ ? redirect_uri: __DEV__ ?
'http://localhost:3000/auth/slack/' : 'http://localhost:3000/auth/slack/' :
'https://www.beautifulatlas.com/auth/slack/', 'https://www.beautifulatlas.com/auth/slack/',
state: this.state.oauthState, state: store.getOauthState(),
}; };
const urlParams = Object.keys(params).map(function(key) { const urlParams = Object.keys(params).map(function(key) {
@ -48,3 +42,5 @@ export default class SlackAuthLink extends React.Component {
) )
} }
} }
export default SlackAuthLink;

View File

@ -6,7 +6,6 @@ import Route from 'react-router/lib/Route';
import IndexRoute from 'react-router/lib/IndexRoute'; import IndexRoute from 'react-router/lib/IndexRoute';
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux'; import { routerMiddleware } from 'react-router-redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger'; import createLogger from 'redux-logger';
import History from 'utils/History'; import History from 'utils/History';
@ -41,42 +40,34 @@ if (__DEV__) {
thunkMiddleware, thunkMiddleware,
routerMiddlewareWithHistory, routerMiddlewareWithHistory,
loggerMiddleware, loggerMiddleware,
), autoRehydrate()); ));
} else { } else {
store = createStore(reducers, applyMiddleware( store = createStore(reducers, applyMiddleware(
thunkMiddleware, thunkMiddleware,
routerMiddlewareWithHistory, routerMiddlewareWithHistory,
), autoRehydrate()); ));
} }
render((
<div style={{ display: 'flex', flex: 1, }}>
<Provider store={store}>
<Router history={History}>
<Route path="/" component={ Application }>
<IndexRoute component={Home} />
persistStore(store, { <Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
whitelist: [ <Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
'user', <Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } />
'team', <Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
] <Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
}, () => {
render((
<div style={{ display: 'flex', flex: 1, }}>
<Provider store={store}>
<Router history={History}>
<Route path="/" component={ Application }>
<IndexRoute component={Home} />
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } /> <Route path="/auth/slack" component={SlackAuth} />
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } /> </Route>
<Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } /> </Router>
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } /> </Provider>
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } /> { __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
</div>
<Route path="/auth/slack" component={SlackAuth} /> ), document.getElementById('root'));
</Route>
</Router>
</Provider>
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
</div>
), document.getElementById('root'));
});
function requireAuth(nextState, replace) { function requireAuth(nextState, replace) {
if (!auth.loggedIn()) { if (!auth.loggedIn()) {

View File

@ -2,12 +2,8 @@ import { combineReducers } from 'redux';
import atlases from './atlases'; import atlases from './atlases';
import document from './document'; import document from './document';
import team from './team';
import user from './user';
export default combineReducers({ export default combineReducers({
atlases, atlases,
document, document,
team,
user,
}); });

View File

@ -1,15 +0,0 @@
import { SLACK_AUTH_SUCCESS } from 'actions/SlackAuthAction';
const team = (state = null, action) => {
switch (action.type) {
case SLACK_AUTH_SUCCESS: {
return {
...action.team,
};
}
default:
return state;
}
};
export default team;

View File

@ -1,15 +0,0 @@
import { SLACK_AUTH_SUCCESS } from 'actions/SlackAuthAction';
const user = (state = null, action) => {
switch (action.type) {
case SLACK_AUTH_SUCCESS: {
return {
...action.user,
};
}
default:
return state;
}
};
export default user;

View File

@ -88,7 +88,7 @@ const documentEditState = new class DocumentEditState {
} }
constructor() { constructor() {
// Rehydrate // Rehydrate syncronously
localforage.getItem(DOCUMENT_EDIT_SETTINGS) localforage.getItem(DOCUMENT_EDIT_SETTINGS)
.then(data => { .then(data => {
this.preview = data.preview; this.preview = data.preview;

View File

@ -1,22 +1,15 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import store from 'stores/UserStore';
import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'
import { replace } from 'react-router-redux';
import auth from '../../utils/auth';
import SlackAuthLink from '../../components/SlackAuthLink'; import SlackAuthLink from '../../components/SlackAuthLink';
import styles from './Home.scss'; import styles from './Home.scss';
class Home extends React.Component { export default class Home extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
componentDidMount = () => { componentDidMount = () => {
if (auth.loggedIn()) { if (store.authenticated) {
this.props.replace('/dashboard'); browserHistory.replace('/dashboard');
} }
} }
@ -53,11 +46,3 @@ class Home extends React.Component {
); );
} }
} }
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
}
export default connect(
null, mapDispatchToProps
)(Home);

View File

@ -1,21 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import store from 'stores/UserStore';
import { bindActionCreators } from 'redux';
import { slackAuthAsync } from '../../actions/SlackAuthAction'; export default class SlackAuth extends React.Component {
import { client } from '../../utils/ApiClient';
class SlackAuth extends React.Component {
componentDidMount = () => { componentDidMount = () => {
const { query } = this.props.location const { code, state } = this.props.location.query;
store.authWithSlack(code, state);
// Validate OAuth2 state param
if (localStorage.oauthState != query.state) {
return;
}
this.props.slackAuthAsync(query.code);
} }
render() { render() {
@ -24,12 +13,3 @@ class SlackAuth extends React.Component {
); );
} }
} }
const mapDispactcToProps = (dispatch) => {
return bindActionCreators({ slackAuthAsync }, dispatch);
};
export default connect(
null,
mapDispactcToProps
)(SlackAuth);

82
src/stores/UserStore.js Normal file
View File

@ -0,0 +1,82 @@
import { observable, action, computed, autorun } from 'mobx';
import { browserHistory } from 'react-router';
import { client } from 'utils/ApiClient';
import localforage from 'localforage';
const USER_STORE = 'USER_STORE';
const store = new class UserStore {
@observable user;
@observable team;
@observable token;
@observable oauthState;
@observable isLoading;
/* Computed */
@computed get authenticated() {
return !!this.token;
}
@computed get asJson() {
return JSON.stringify({
user: this.user,
team: this.team,
token: this.token,
oauthState: this.oauthState,
});
}
/* Actions */
@action logout = () => {
this.user = null;
this.token = null;
browserHistory.push('/');
};
@action getOauthState = () => {
const state = Math.random().toString(36).substring(7);
this.oauthState = state;
return this.oauthState;
}
@action authWithSlack = async (code, state) => {
if (state !== this.oauthState) {
browserHistory.push('/auth-error');
return;
}
let res;
try {
res = await client.post('/auth.slack', { code: code });
} catch (e) {
browserHistory.push('/auth-error');
return;
}
this.user = res.data.user;
this.team = res.data.team;
this.token = res.data.accessToken;
browserHistory.replace('/dashboard');
}
constructor() {
// Rehydrate
const data = JSON.parse(localStorage.getItem(USER_STORE)) || {};
this.user = data.user;
this.team = data.team;
this.token = data.token;
this.oauthState = data.oauthState;
}
}();
// Persist store to localStorage
autorun(() => {
localStorage.setItem(USER_STORE, store.asJson);
});
export default store;

View File

@ -1,6 +1,6 @@
import _map from 'lodash/map'; import _map from 'lodash/map';
import store from 'stores/UserStore';
import auth from './auth';
import constants from '../constants'; import constants from '../constants';
class ApiClient { class ApiClient {
@ -25,8 +25,8 @@ class ApiClient {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': this.userAgent, 'User-Agent': this.userAgent,
}); });
if (auth.getToken()) { if (store.authenticated) {
headers.set('Authorization', `Bearer ${auth.getToken()}`); headers.set('Authorization', `Bearer ${store.token}`);
} }
// Construct request // Construct request
@ -48,7 +48,7 @@ class ApiClient {
// Handle 401, log out user // Handle 401, log out user
if (response.status === 401) { if (response.status === 401) {
auth.logout(); // replace with dispatch+action store.logout();
} }
// Handle failed responses // Handle failed responses