Login workflow (#137)
* Use collection label instead of name on the CollectionPage * Added Avatar and logout menu item * [feat](login) Added userpic with a logout action in the dropdown. - Display logged in user in the AppHeader - Implemented logout action and store + tests - Better styles for GitHub sign in screen Closes #100 * Better styles for the AppHeader
This commit is contained in:
parent
1c4751f479
commit
4d696f2253
@ -3,17 +3,18 @@ import { currentBackend } from '../backends/backend';
|
||||
export const AUTH_REQUEST = 'AUTH_REQUEST';
|
||||
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
|
||||
export const AUTH_FAILURE = 'AUTH_FAILURE';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export function authenticating() {
|
||||
return {
|
||||
type: AUTH_REQUEST
|
||||
type: AUTH_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
export function authenticate(userData) {
|
||||
return {
|
||||
type: AUTH_SUCCESS,
|
||||
payload: userData
|
||||
payload: userData,
|
||||
};
|
||||
}
|
||||
|
||||
@ -25,6 +26,12 @@ export function authError(error) {
|
||||
};
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return {
|
||||
type: LOGOUT,
|
||||
};
|
||||
}
|
||||
|
||||
export function loginUser(credentials) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@ -32,6 +39,17 @@ export function loginUser(credentials) {
|
||||
|
||||
dispatch(authenticating());
|
||||
return backend.authenticate(credentials)
|
||||
.then((user) => dispatch(authenticate(user)));
|
||||
.then((user) => {
|
||||
dispatch(authenticate(user));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function logoutUser() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
backend.logout();
|
||||
dispatch(logout());
|
||||
};
|
||||
}
|
||||
|
@ -16,6 +16,10 @@ class LocalStorageAuthStore {
|
||||
store(userData) {
|
||||
window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
|
||||
}
|
||||
|
||||
logout() {
|
||||
window.localStorage.removeItem(this.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
const slugFormatter = (template, entryData) => {
|
||||
@ -67,6 +71,14 @@ class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (this.authStore) {
|
||||
this.authStore.logout();
|
||||
} else {
|
||||
throw new Error('User isn\'t authenticated.');
|
||||
}
|
||||
}
|
||||
|
||||
listEntries(collection) {
|
||||
const collectionModel = new Collection(collection);
|
||||
const listMethod = this.implementation[collectionModel.listMethod()];
|
||||
|
11
src/backends/github/AuthenticationPage.css
Normal file
11
src/backends/github/AuthenticationPage.css
Normal file
@ -0,0 +1,11 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: .25em 1em;
|
||||
height: auto;
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import Button from 'react-toolbox/lib/button';
|
||||
import Authenticator from '../../lib/netlify-auth';
|
||||
import { Icon } from '../../components/UI';
|
||||
import styles from './AuthenticationPage.css';
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: React.PropTypes.func.isRequired
|
||||
onLogin: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
handleLogin = e => {
|
||||
handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
let auth;
|
||||
if (document.location.host.split(':')[0] === 'localhost') {
|
||||
@ -29,9 +32,17 @@ export default class AuthenticationPage extends React.Component {
|
||||
render() {
|
||||
const { loginError } = this.state;
|
||||
|
||||
return <div>
|
||||
{loginError && <p>{loginError}</p>}
|
||||
<p><a href="#" onClick={this.handleLogin}>Login with GitHub</a></p>
|
||||
</div>;
|
||||
return (
|
||||
<section className={styles.root}>
|
||||
{loginError && <p>{loginError}</p>}
|
||||
<Button
|
||||
className={styles.button}
|
||||
raised
|
||||
onClick={this.handleLogin}
|
||||
>
|
||||
<Icon type="github" /> Login with GitHub
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,3 +9,11 @@
|
||||
.appBar {
|
||||
background-color: var(--backgroundColor);
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* stylelint-disable */
|
||||
/* Cascade is evil :( */
|
||||
color: var(--foregroundColor) !important;
|
||||
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
@ -2,24 +2,29 @@ import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import pluralize from 'pluralize';
|
||||
import { IndexLink } from 'react-router';
|
||||
import { Menu, MenuItem } from 'react-toolbox';
|
||||
import { IconMenu, Menu, MenuItem } from 'react-toolbox/lib/menu';
|
||||
import Avatar from 'react-toolbox/lib/avatar';
|
||||
import AppBar from 'react-toolbox/lib/app_bar';
|
||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||
import FindBar from '../FindBar/FindBar';
|
||||
import styles from './AppHeader.css';
|
||||
|
||||
export default class AppHeader extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
user: ImmutablePropTypes.map.isRequired,
|
||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||
commands: PropTypes.array.isRequired, // eslint-disable-line
|
||||
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
toggleNavDrawer: PropTypes.func.isRequired,
|
||||
onCreateEntryClick: PropTypes.func.isRequired,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
createMenuActive: false,
|
||||
userMenuActive: false,
|
||||
};
|
||||
|
||||
handleCreatePostClick = (collectionName) => {
|
||||
@ -41,27 +46,53 @@ export default class AppHeader extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleRightIconClick = () => {
|
||||
this.setState({
|
||||
userMenuActive: !this.state.userMenuActive,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
collections,
|
||||
commands,
|
||||
defaultCommands,
|
||||
runCommand,
|
||||
toggleNavDrawer,
|
||||
onLogoutClick,
|
||||
} = this.props;
|
||||
const { createMenuActive } = this.state;
|
||||
|
||||
const {
|
||||
createMenuActive,
|
||||
userMenuActive,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
fixed
|
||||
theme={styles}
|
||||
leftIcon="menu"
|
||||
rightIcon="create"
|
||||
rightIcon={
|
||||
<div>
|
||||
<Avatar
|
||||
title={user.get('name')}
|
||||
image={user.get('avatar_url')}
|
||||
/>
|
||||
<Menu
|
||||
active={userMenuActive}
|
||||
position="topRight"
|
||||
onHide={this.handleRightIconClick}
|
||||
>
|
||||
<MenuItem onClick={onLogoutClick}>Log out</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
}
|
||||
onLeftIconClick={toggleNavDrawer}
|
||||
onRightIconClick={this.handleCreateButtonClick}
|
||||
onRightIconClick={this.handleRightIconClick}
|
||||
>
|
||||
<IndexLink to="/">
|
||||
Dashboard
|
||||
<FontIcon value="home" />
|
||||
</IndexLink>
|
||||
|
||||
<FindBar
|
||||
@ -69,9 +100,11 @@ export default class AppHeader extends React.Component {
|
||||
defaultCommands={defaultCommands}
|
||||
runCommand={runCommand}
|
||||
/>
|
||||
<Menu
|
||||
<IconMenu
|
||||
theme={styles}
|
||||
active={createMenuActive}
|
||||
position="topRight"
|
||||
icon="create"
|
||||
onClick={this.handleCreateButtonClick}
|
||||
onHide={this.handleCreateMenuHide}
|
||||
>
|
||||
{
|
||||
@ -84,7 +117,7 @@ export default class AppHeader extends React.Component {
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
</IconMenu>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { Link } from 'react-toolbox/lib/link';
|
||||
import { Notifs } from 'redux-notifications';
|
||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||
import { loadConfig } from '../actions/config';
|
||||
import { loginUser } from '../actions/auth';
|
||||
import { loginUser, logoutUser } from '../actions/auth';
|
||||
import { currentBackend } from '../backends/backend';
|
||||
import {
|
||||
SHOW_COLLECTION,
|
||||
@ -40,6 +40,7 @@ class App extends React.Component {
|
||||
config: ImmutablePropTypes.map,
|
||||
collections: ImmutablePropTypes.orderedMap,
|
||||
createNewEntryInCollection: PropTypes.func.isRequired,
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
navigateToCollection: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map,
|
||||
@ -138,6 +139,7 @@ class App extends React.Component {
|
||||
runCommand,
|
||||
navigateToCollection,
|
||||
createNewEntryInCollection,
|
||||
logoutUser,
|
||||
isFetching,
|
||||
} = this.props;
|
||||
|
||||
@ -189,11 +191,13 @@ class App extends React.Component {
|
||||
</nav>
|
||||
</NavDrawer>
|
||||
<AppHeader
|
||||
user={user}
|
||||
collections={collections}
|
||||
commands={commands}
|
||||
defaultCommands={defaultCommands}
|
||||
runCommand={runCommand}
|
||||
onCreateEntryClick={createNewEntryInCollection}
|
||||
onLogoutClick={logoutUser}
|
||||
toggleNavDrawer={this.toggleNavDrawer}
|
||||
/>
|
||||
<Panel scrollY>
|
||||
@ -226,6 +230,9 @@ function mapDispatchToProps(dispatch) {
|
||||
createNewEntryInCollection: (collectionName) => {
|
||||
dispatch(createNewEntryInCollection(collectionName));
|
||||
},
|
||||
logoutUser: () => {
|
||||
dispatch(logoutUser());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import expect from 'expect';
|
||||
import Immutable from 'immutable';
|
||||
import { authenticating, authenticate, authError } from '../../actions/auth';
|
||||
import { authenticating, authenticate, authError, logout } from '../../actions/auth';
|
||||
import auth from '../auth';
|
||||
|
||||
describe('auth', () => {
|
||||
@ -33,8 +32,14 @@ describe('auth', () => {
|
||||
auth(undefined, authError(new Error('Bad credentials')))
|
||||
).toEqual(
|
||||
Immutable.Map({
|
||||
error: 'Error: Bad credentials'
|
||||
error: 'Error: Bad credentials',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle logout', () => {
|
||||
const initialState = Immutable.fromJS({ user: { email: 'joe@example.com' } });
|
||||
const newState = auth(initialState, logout());
|
||||
expect(newState.get('user')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Immutable from 'immutable';
|
||||
import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from '../actions/auth';
|
||||
import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE, LOGOUT } from '../actions/auth';
|
||||
|
||||
const auth = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
@ -9,6 +9,8 @@ const auth = (state = null, action) => {
|
||||
return Immutable.fromJS({ user: action.payload });
|
||||
case AUTH_FAILURE:
|
||||
return Immutable.Map({ error: action.payload.toString() });
|
||||
case LOGOUT:
|
||||
return state.remove('user');
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user