From 0d87d6abf5bec5ceea8924fd166cc8b93e06c283 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 29 May 2017 19:08:03 -0700 Subject: [PATCH] Separated user and auth stores --- frontend/components/Layout/Layout.js | 27 ++--- .../components/SlackAuthLink/SlackAuthLink.js | 8 +- frontend/index.js | 6 +- frontend/scenes/Home/Home.js | 43 +++++--- frontend/scenes/Home/Home.scss | 9 -- frontend/scenes/SlackAuth/SlackAuth.js | 19 ++-- frontend/stores/AuthStore.js | 100 ++++++++++++++++++ frontend/stores/CollectionsStore.js | 2 + frontend/stores/UiStore.js | 7 +- frontend/stores/UserStore.js | 75 ++----------- frontend/stores/index.js | 15 +-- frontend/utils/ApiClient.js | 23 ++-- 12 files changed, 191 insertions(+), 143 deletions(-) delete mode 100644 frontend/scenes/Home/Home.scss create mode 100644 frontend/stores/AuthStore.js diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 3945f341..2d6aec85 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -13,6 +13,7 @@ import { textColor, headerHeight } from 'styles/constants.scss'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import LoadingIndicator from 'components/LoadingIndicator'; import UserStore from 'stores/UserStore'; +import AuthStore from 'stores/AuthStore'; type Props = { history: Object, @@ -21,6 +22,7 @@ type Props = { title?: ?React.Element, loading?: boolean, user: UserStore, + auth: AuthStore, search: ?boolean, notifications?: React.Element, }; @@ -34,22 +36,22 @@ type Props = { @keydown(['/', 't']) search() { - if (!this.props.user) return; - _.defer(() => this.props.history.push('/search')); + if (this.props.auth.isAuthenticated) + _.defer(() => this.props.history.push('/search')); } @keydown(['d']) dashboard() { - if (!this.props.user) return; - _.defer(() => this.props.history.push('/')); + if (this.props.auth.isAuthenticated) + _.defer(() => this.props.history.push('/')); } - render() { - const user = this.props.user; + handleLogout = () => { + this.props.auth.logout(() => this.props.history.push('/')); + }; - const handleLogout = () => { - user.logout(() => this.props.history.push('/')); - }; + render() { + const { auth, user } = this.props; return ( @@ -79,7 +81,8 @@ type Props = { {this.props.actions} - {user.user && + {auth.authenticated && + user && {this.props.search && @@ -101,7 +104,7 @@ type Props = { API - Logout + Logout } @@ -182,4 +185,4 @@ const Content = styled(Flex)` overflow: scroll; `; -export default withRouter(inject('user')(Layout)); +export default withRouter(inject('user', 'auth')(Layout)); diff --git a/frontend/components/SlackAuthLink/SlackAuthLink.js b/frontend/components/SlackAuthLink/SlackAuthLink.js index 69348e2b..d8225c67 100644 --- a/frontend/components/SlackAuthLink/SlackAuthLink.js +++ b/frontend/components/SlackAuthLink/SlackAuthLink.js @@ -1,12 +1,12 @@ // @flow import React from 'react'; import { observer, inject } from 'mobx-react'; -import UserStore from 'stores/UserStore'; +import AuthStore from 'stores/AuthStore'; type Props = { children: React.Element, scopes?: Array, - user: UserStore, + auth: AuthStore, redirectUri: string, }; @@ -28,7 +28,7 @@ type Props = { client_id: SLACK_KEY, scope: this.props.scopes ? this.props.scopes.join(' ') : '', redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI, - state: this.props.user.getOauthState(), + state: this.props.auth.getOauthState(), }; const urlParams = Object.keys(params) @@ -45,4 +45,4 @@ type Props = { } } -export default inject('user')(SlackAuthLink); +export default inject('auth')(SlackAuthLink); diff --git a/frontend/index.js b/frontend/index.js index 96436d86..901089c8 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -45,15 +45,17 @@ type AuthProps = { }; const Auth = ({ children }: AuthProps) => { - if (stores.user.authenticated && stores.user.team) { + if (stores.auth.authenticated && stores.auth.team) { // Only initialize stores once. Kept in global scope // because otherwise they will get overriden on route // change if (!authenticatedStores) { // Stores for authenticated user + const user = stores.auth.getUserStore(); authenticatedStores = { + user, collections: new CollectionsStore({ - teamId: stores.user.team.id, + teamId: user.team.id, }), }; diff --git a/frontend/scenes/Home/Home.js b/frontend/scenes/Home/Home.js index 5f16717d..4cf12891 100644 --- a/frontend/scenes/Home/Home.js +++ b/frontend/scenes/Home/Home.js @@ -2,22 +2,23 @@ import React from 'react'; import { observer, inject } from 'mobx-react'; import { Redirect } from 'react-router'; - import { Flex } from 'reflexbox'; +import styled from 'styled-components'; + +import AuthStore from 'stores/AuthStore'; + import Layout from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import SlackAuthLink from 'components/SlackAuthLink'; import Alert from 'components/Alert'; -import styles from './Home.scss'; +type Props = { + auth: AuthStore, + location: Object, +}; -@inject('user') -@observer -export default class Home extends React.Component { - static propTypes = { - user: React.PropTypes.object.isRequired, - location: React.PropTypes.object.isRequired, - }; +@observer class Home extends React.Component { + props: Props; get notifications(): React.Element[] { const notifications = []; @@ -40,17 +41,17 @@ export default class Home extends React.Component { return ( - {this.props.user.authenticated && } + {this.props.auth.authenticated && } {showLandingPageCopy && -
-

Simple, fast, markdown.

-

+

+ Simple, fast, markdown. + We're building a modern wiki for engineering teams. -

+
} -
+
Sign in with Slack { + this.user = null; + this.token = null; + cb(); + }; + + @action getOauthState = () => { + const state = Math.random().toString(36).substring(7); + this.oauthState = state; + return this.oauthState; + }; + + @action authWithSlack = async (code: string, state: string) => { + if (state !== this.oauthState) { + return { + success: false, + }; + } + + let res; + try { + res = await client.post('/auth.slack', { code }); + } catch (e) { + return { + success: false, + }; + } + + invariant( + res && res.data && res.data.user && res.data.team && res.data.accessToken, + 'All values should be available' + ); + this.user = res.data.user; + this.team = res.data.team; + this.token = res.data.accessToken; + + return { + success: true, + }; + }; + + getUserStore(): UserStore { + invariant( + this.user && this.team, + 'Tried to create a user store without data' + ); + return new UserStore({ + user: this.user, + team: this.team, + }); + } + + constructor() { + // Rehydrate + const data = JSON.parse(localStorage.getItem(AUTH_STORE) || '{}'); + this.user = data.user; + this.team = data.team; + this.token = data.token; + this.oauthState = data.oauthState; + + autorunAsync(() => { + localStorage.setItem(AUTH_STORE, this.asJson); + }); + } +} + +export default AuthStore; diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js index 053e7121..ee653402 100644 --- a/frontend/stores/CollectionsStore.js +++ b/frontend/stores/CollectionsStore.js @@ -2,6 +2,7 @@ import { observable, action, runInAction, ObservableArray } from 'mobx'; import ApiClient, { client } from 'utils/ApiClient'; import _ from 'lodash'; +import invariant from 'invariant'; import stores from 'stores'; import Collection from 'models/Collection'; @@ -26,6 +27,7 @@ class CollectionsStore { const res = await this.client.post('/collections.list', { id: this.teamId, }); + invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { this.data.replace( diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js index 64b795dc..80c95352 100644 --- a/frontend/stores/UiStore.js +++ b/frontend/stores/UiStore.js @@ -1,5 +1,5 @@ // @flow -import { observable, action, computed } from 'mobx'; +import { observable, action, computed, autorunAsync } from 'mobx'; const UI_STORE = 'UI_STORE'; @@ -24,8 +24,11 @@ class UiStore { // Rehydrate const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}'); this.sidebar = data.sidebar; + + autorunAsync(() => { + localStorage.setItem(UI_STORE, this.asJson); + }); } } export default UiStore; -export { UI_STORE }; diff --git a/frontend/stores/UserStore.js b/frontend/stores/UserStore.js index 30a90845..7d368c93 100644 --- a/frontend/stores/UserStore.js +++ b/frontend/stores/UserStore.js @@ -1,87 +1,32 @@ // @flow -import { observable, action, computed } from 'mobx'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; +import { observable, computed } from 'mobx'; import type { User, Team } from 'types'; -const USER_STORE = 'USER_STORE'; +type Options = { + user: User, + team: Team, +}; class UserStore { - @observable user: ?User; - @observable team: ?Team; - - @observable token: ?string; - @observable oauthState: string; + @observable user: User; + @observable team: Team; @observable isLoading: boolean = false; /* Computed */ - @computed get authenticated(): boolean { - return !!this.token; - } - @computed get asJson(): string { return JSON.stringify({ user: this.user, team: this.team, - token: this.token, - oauthState: this.oauthState, }); } - /* Actions */ - - @action logout = (cb: Function) => { - this.user = null; - this.token = null; - cb(); - }; - - @action getOauthState = () => { - const state = Math.random().toString(36).substring(7); - this.oauthState = state; - return this.oauthState; - }; - - @action authWithSlack = async (code: string, state: string) => { - if (state !== this.oauthState) { - return { - success: false, - }; - } - - let res; - try { - res = await client.post('/auth.slack', { code }); - } catch (e) { - return { - success: false, - }; - } - - invariant( - res && res.data && res.data.user && res.data.team && res.data.accessToken, - 'All values should be available' - ); - this.user = res.data.user; - this.team = res.data.team; - this.token = res.data.accessToken; - - return { - success: true, - }; - }; - - constructor() { + constructor(options: Options) { // 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; + this.user = options.user; + this.team = options.team; } } export default UserStore; -export { USER_STORE }; diff --git a/frontend/stores/index.js b/frontend/stores/index.js index 8fd69701..70eb0853 100644 --- a/frontend/stores/index.js +++ b/frontend/stores/index.js @@ -1,20 +1,13 @@ // @flow -import { autorunAsync } from 'mobx'; -import UserStore, { USER_STORE } from './UserStore'; -import UiStore, { UI_STORE } from './UiStore'; +import AuthStore from './AuthStore'; +import UiStore from './UiStore'; import ErrorsStore from './ErrorsStore'; const stores = { - user: new UserStore(), + user: null, // Including for Layout + auth: new AuthStore(), ui: new UiStore(), errors: new ErrorsStore(), }; -// Persist stores to localStorage -// TODO: move to store constructors -autorunAsync(() => { - localStorage.setItem(USER_STORE, stores.user.asJson); - localStorage.setItem(UI_STORE, stores.ui.asJson); -}); - export default stores; diff --git a/frontend/utils/ApiClient.js b/frontend/utils/ApiClient.js index 4c304cc6..5abf98ef 100644 --- a/frontend/utils/ApiClient.js +++ b/frontend/utils/ApiClient.js @@ -1,5 +1,6 @@ // @flow import _ from 'lodash'; +import invariant from 'invariant'; import stores from 'stores'; type Options = { @@ -39,9 +40,9 @@ class ApiClient { Accept: 'application/json', 'Content-Type': 'application/json', }); - if (stores.user.authenticated) { - // $FlowFixMe this is not great, need to refactor - headers.set('Authorization', `Bearer ${stores.user.token}`); + if (stores.auth.authenticated) { + invariant(stores.auth.token, 'JWT token not set properly'); + headers.set('Authorization', `Bearer ${stores.auth.token}`); } // Construct request @@ -62,15 +63,10 @@ class ApiClient { return response; } - // // Handle 404 - // if (response.status === 404) { - // // return browserHistory.push('/404'); - // } - - // // Handle 401, log out user - // if (response.status === 401) { - // return stores.user.logout(); - // } + // Handle 401, log out user + if (response.status === 401) { + return stores.auth.logout(() => (window.location = '/')); + } // Handle failed responses const error = {}; @@ -112,5 +108,4 @@ class ApiClient { export default ApiClient; // In case you don't want to always initiate, just import with `import { client } ...` -const client = new ApiClient(); -export { client }; +export const client = new ApiClient();