Merge pull request #6 from jorilallo/jori-mobx-migration
Migrated user/auth from redux to mobx
This commit is contained in:
@ -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'));
|
||||
})
|
||||
};
|
||||
};
|
@ -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('/'));
|
||||
};
|
||||
};
|
@ -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;
|
@ -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;
|
||||
|
49
src/index.js
49
src/index.js
@ -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,42 +40,34 @@ if (__DEV__) {
|
||||
thunkMiddleware,
|
||||
routerMiddlewareWithHistory,
|
||||
loggerMiddleware,
|
||||
), autoRehydrate());
|
||||
));
|
||||
} else {
|
||||
store = createStore(reducers, applyMiddleware(
|
||||
thunkMiddleware,
|
||||
routerMiddlewareWithHistory,
|
||||
), autoRehydrate());
|
||||
));
|
||||
}
|
||||
|
||||
render((
|
||||
<div style={{ display: 'flex', flex: 1, }}>
|
||||
<Provider store={store}>
|
||||
<Router history={History}>
|
||||
<Route path="/" component={ Application }>
|
||||
<IndexRoute component={Home} />
|
||||
|
||||
persistStore(store, {
|
||||
whitelist: [
|
||||
'user',
|
||||
'team',
|
||||
]
|
||||
}, () => {
|
||||
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="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } />
|
||||
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
|
||||
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
|
||||
|
||||
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } />
|
||||
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
|
||||
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
|
||||
|
||||
<Route path="/auth/slack" component={SlackAuth} />
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
|
||||
</div>
|
||||
), document.getElementById('root'));
|
||||
});
|
||||
<Route path="/auth/slack" component={SlackAuth} />
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
|
||||
</div>
|
||||
), document.getElementById('root'));
|
||||
|
||||
function requireAuth(nextState, replace) {
|
||||
if (!auth.loggedIn()) {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
@ -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;
|
@ -88,7 +88,7 @@ const documentEditState = new class DocumentEditState {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Rehydrate
|
||||
// Rehydrate syncronously
|
||||
localforage.getItem(DOCUMENT_EDIT_SETTINGS)
|
||||
.then(data => {
|
||||
this.preview = data.preview;
|
||||
|
@ -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);
|
@ -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
82
src/stores/UserStore.js
Normal 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;
|
@ -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
|
||||
|
Reference in New Issue
Block a user