Separated user and auth stores
This commit is contained in:
@ -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<any>,
|
||||
loading?: boolean,
|
||||
user: UserStore,
|
||||
auth: AuthStore,
|
||||
search: ?boolean,
|
||||
notifications?: React.Element<any>,
|
||||
};
|
||||
@ -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 (
|
||||
<Container column auto>
|
||||
@ -79,7 +81,8 @@ type Props = {
|
||||
<Flex align="center">
|
||||
{this.props.actions}
|
||||
</Flex>
|
||||
{user.user &&
|
||||
{auth.authenticated &&
|
||||
user &&
|
||||
<Flex>
|
||||
{this.props.search &&
|
||||
<Flex>
|
||||
@ -101,7 +104,7 @@ type Props = {
|
||||
<MenuLink to="/developers">
|
||||
<MenuItem>API</MenuItem>
|
||||
</MenuLink>
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
|
||||
</DropdownMenu>
|
||||
</Flex>}
|
||||
</Flex>
|
||||
@ -182,4 +185,4 @@ const Content = styled(Flex)`
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('user')(Layout));
|
||||
export default withRouter(inject('user', 'auth')(Layout));
|
||||
|
@ -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<any>,
|
||||
scopes?: Array<string>,
|
||||
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);
|
||||
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -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<any>[] {
|
||||
const notifications = [];
|
||||
@ -40,17 +41,17 @@ export default class Home extends React.Component {
|
||||
return (
|
||||
<Flex auto>
|
||||
<Layout notifications={this.notifications}>
|
||||
{this.props.user.authenticated && <Redirect to="/dashboard" />}
|
||||
{this.props.auth.authenticated && <Redirect to="/dashboard" />}
|
||||
|
||||
<CenteredContent>
|
||||
{showLandingPageCopy &&
|
||||
<div className={styles.intro}>
|
||||
<h1 className={styles.title}>Simple, fast, markdown.</h1>
|
||||
<p className={styles.copy}>
|
||||
<div>
|
||||
<Title>Simple, fast, markdown.</Title>
|
||||
<Copy>
|
||||
We're building a modern wiki for engineering teams.
|
||||
</p>
|
||||
</Copy>
|
||||
</div>}
|
||||
<div className={styles.action}>
|
||||
<div>
|
||||
<SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
|
||||
<img
|
||||
alt="Sign in with Slack"
|
||||
@ -67,3 +68,15 @@ export default class Home extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 36px;
|
||||
margin-bottom: 24px;
|
||||
}`;
|
||||
|
||||
const Copy = styled.p`
|
||||
font-size: 20px;
|
||||
margin-bottom: 36px;
|
||||
}`;
|
||||
|
||||
export default inject('auth')(Home);
|
||||
|
@ -1,9 +0,0 @@
|
||||
.title {
|
||||
font-size: 36px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
font-size: 20px;
|
||||
margin-bottom: 36px;
|
||||
}
|
@ -5,19 +5,20 @@ import queryString from 'query-string';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { client } from 'utils/ApiClient';
|
||||
|
||||
import UserStore from 'stores/UserStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
user: UserStore,
|
||||
auth: AuthStore,
|
||||
location: Object,
|
||||
};
|
||||
|
||||
@inject('user')
|
||||
@observer
|
||||
class SlackAuth extends React.Component {
|
||||
props: Props;
|
||||
type State = {
|
||||
redirectTo: string,
|
||||
};
|
||||
|
||||
state: { redirectTo?: string };
|
||||
@observer class SlackAuth extends React.Component {
|
||||
props: Props;
|
||||
state: State;
|
||||
state = {};
|
||||
|
||||
// $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803
|
||||
@ -47,7 +48,7 @@ class SlackAuth extends React.Component {
|
||||
const redirectTo = sessionStorage.getItem('redirectTo');
|
||||
sessionStorage.removeItem('redirectTo');
|
||||
|
||||
const { success } = await this.props.user.authWithSlack(code, state);
|
||||
const { success } = await this.props.auth.authWithSlack(code, state);
|
||||
success
|
||||
? this.setState({ redirectTo: redirectTo || '/dashboard' })
|
||||
: this.setState({ redirectTo: '/auth/error' });
|
||||
@ -64,4 +65,4 @@ class SlackAuth extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default SlackAuth;
|
||||
export default inject('auth')(SlackAuth);
|
||||
|
100
frontend/stores/AuthStore.js
Normal file
100
frontend/stores/AuthStore.js
Normal file
@ -0,0 +1,100 @@
|
||||
// @flow
|
||||
import { observable, action, computed, autorunAsync } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import UserStore from 'stores/UserStore';
|
||||
import type { User, Team } from 'types';
|
||||
|
||||
const AUTH_STORE = 'AUTH_STORE';
|
||||
|
||||
class AuthStore {
|
||||
@observable user: ?User;
|
||||
@observable team: ?Team;
|
||||
@observable token: ?string;
|
||||
@observable oauthState: string;
|
||||
@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,
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
@ -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(
|
||||
|
@ -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 };
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user