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

View File

@ -1,16 +1,14 @@
import React from 'react';
import { observe } from 'mobx'
import store from 'stores/UserStore';
import styles from './SlackAuthLink.scss';
export default class SlackAuthLink extends React.Component {
class SlackAuthLink extends React.Component {
static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string),
}
state = {
oauthState: Math.random().toString(36).substring(7),
}
static defaultProps = {
scopes: [
'identity.email',
@ -20,10 +18,6 @@ export default class SlackAuthLink extends React.Component {
]
}
componentDidMount = () => {
localStorage.oauthState = this.state.oauthState;
}
slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
@ -32,7 +26,7 @@ export default class SlackAuthLink extends React.Component {
redirect_uri: __DEV__ ?
'http://localhost:3000/auth/slack/' :
'https://www.beautifulatlas.com/auth/slack/',
state: this.state.oauthState,
state: store.getOauthState(),
};
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 { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import History from 'utils/History';
@ -41,21 +40,14 @@ if (__DEV__) {
thunkMiddleware,
routerMiddlewareWithHistory,
loggerMiddleware,
), autoRehydrate());
));
} else {
store = createStore(reducers, applyMiddleware(
thunkMiddleware,
routerMiddlewareWithHistory,
), autoRehydrate());
));
}
persistStore(store, {
whitelist: [
'user',
'team',
]
}, () => {
render((
<div style={{ display: 'flex', flex: 1, }}>
<Provider store={store}>
@ -76,7 +68,6 @@ persistStore(store, {
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
</div>
), document.getElementById('root'));
});
function requireAuth(nextState, replace) {
if (!auth.loggedIn()) {

View File

@ -2,12 +2,8 @@ import { combineReducers } from 'redux';
import atlases from './atlases';
import document from './document';
import team from './team';
import user from './user';
export default combineReducers({
atlases,
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() {
// Rehydrate
// Rehydrate syncronously
localforage.getItem(DOCUMENT_EDIT_SETTINGS)
.then(data => {
this.preview = data.preview;

View File

@ -1,22 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import auth from '../../utils/auth';
import store from 'stores/UserStore';
import { browserHistory } from 'react-router'
import SlackAuthLink from '../../components/SlackAuthLink';
import styles from './Home.scss';
class Home extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
export default class Home extends React.Component {
componentDidMount = () => {
if (auth.loggedIn()) {
this.props.replace('/dashboard');
if (store.authenticated) {
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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import store from 'stores/UserStore';
import { slackAuthAsync } from '../../actions/SlackAuthAction';
import { client } from '../../utils/ApiClient';
class SlackAuth extends React.Component {
export default class SlackAuth extends React.Component {
componentDidMount = () => {
const { query } = this.props.location
// Validate OAuth2 state param
if (localStorage.oauthState != query.state) {
return;
}
this.props.slackAuthAsync(query.code);
const { code, state } = this.props.location.query;
store.authWithSlack(code, state);
}
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 store from 'stores/UserStore';
import auth from './auth';
import constants from '../constants';
class ApiClient {
@ -25,8 +25,8 @@ class ApiClient {
'Content-Type': 'application/json',
'User-Agent': this.userAgent,
});
if (auth.getToken()) {
headers.set('Authorization', `Bearer ${auth.getToken()}`);
if (store.authenticated) {
headers.set('Authorization', `Bearer ${store.token}`);
}
// Construct request
@ -48,7 +48,7 @@ class ApiClient {
// Handle 401, log out user
if (response.status === 401) {
auth.logout(); // replace with dispatch+action
store.logout();
}
// Handle failed responses