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 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);
|
|
@ -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;
|
||||||
|
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 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()) {
|
||||||
|
@ -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,
|
|
||||||
});
|
});
|
||||||
|
@ -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() {
|
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;
|
||||||
|
@ -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);
|
|
@ -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
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 _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
|
||||||
|
Reference in New Issue
Block a user