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_REQUEST = 'AUTH_REQUEST';
|
||||||
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
|
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
|
||||||
export const AUTH_FAILURE = 'AUTH_FAILURE';
|
export const AUTH_FAILURE = 'AUTH_FAILURE';
|
||||||
|
export const LOGOUT = 'LOGOUT';
|
||||||
|
|
||||||
export function authenticating() {
|
export function authenticating() {
|
||||||
return {
|
return {
|
||||||
type: AUTH_REQUEST
|
type: AUTH_REQUEST,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authenticate(userData) {
|
export function authenticate(userData) {
|
||||||
return {
|
return {
|
||||||
type: AUTH_SUCCESS,
|
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) {
|
export function loginUser(credentials) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
@ -32,6 +39,17 @@ export function loginUser(credentials) {
|
|||||||
|
|
||||||
dispatch(authenticating());
|
dispatch(authenticating());
|
||||||
return backend.authenticate(credentials)
|
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) {
|
store(userData) {
|
||||||
window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
|
window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
window.localStorage.removeItem(this.storageKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const slugFormatter = (template, entryData) => {
|
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) {
|
listEntries(collection) {
|
||||||
const collectionModel = new Collection(collection);
|
const collectionModel = new Collection(collection);
|
||||||
const listMethod = this.implementation[collectionModel.listMethod()];
|
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 React from 'react';
|
||||||
|
import Button from 'react-toolbox/lib/button';
|
||||||
import Authenticator from '../../lib/netlify-auth';
|
import Authenticator from '../../lib/netlify-auth';
|
||||||
|
import { Icon } from '../../components/UI';
|
||||||
|
import styles from './AuthenticationPage.css';
|
||||||
|
|
||||||
export default class AuthenticationPage extends React.Component {
|
export default class AuthenticationPage extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onLogin: React.PropTypes.func.isRequired
|
onLogin: React.PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {};
|
state = {};
|
||||||
|
|
||||||
handleLogin = e => {
|
handleLogin = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let auth;
|
let auth;
|
||||||
if (document.location.host.split(':')[0] === 'localhost') {
|
if (document.location.host.split(':')[0] === 'localhost') {
|
||||||
@ -29,9 +32,17 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { loginError } = this.state;
|
const { loginError } = this.state;
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
|
<section className={styles.root}>
|
||||||
{loginError && <p>{loginError}</p>}
|
{loginError && <p>{loginError}</p>}
|
||||||
<p><a href="#" onClick={this.handleLogin}>Login with GitHub</a></p>
|
<Button
|
||||||
</div>;
|
className={styles.button}
|
||||||
|
raised
|
||||||
|
onClick={this.handleLogin}
|
||||||
|
>
|
||||||
|
<Icon type="github" /> Login with GitHub
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,3 +9,11 @@
|
|||||||
.appBar {
|
.appBar {
|
||||||
background-color: var(--backgroundColor);
|
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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { IndexLink } from 'react-router';
|
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 AppBar from 'react-toolbox/lib/app_bar';
|
||||||
|
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||||
import FindBar from '../FindBar/FindBar';
|
import FindBar from '../FindBar/FindBar';
|
||||||
import styles from './AppHeader.css';
|
import styles from './AppHeader.css';
|
||||||
|
|
||||||
export default class AppHeader extends React.Component {
|
export default class AppHeader extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
user: ImmutablePropTypes.map.isRequired,
|
||||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
commands: PropTypes.array.isRequired, // eslint-disable-line
|
commands: PropTypes.array.isRequired, // eslint-disable-line
|
||||||
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
||||||
runCommand: PropTypes.func.isRequired,
|
runCommand: PropTypes.func.isRequired,
|
||||||
toggleNavDrawer: PropTypes.func.isRequired,
|
toggleNavDrawer: PropTypes.func.isRequired,
|
||||||
onCreateEntryClick: PropTypes.func.isRequired,
|
onCreateEntryClick: PropTypes.func.isRequired,
|
||||||
|
onLogoutClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
createMenuActive: false,
|
createMenuActive: false,
|
||||||
|
userMenuActive: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCreatePostClick = (collectionName) => {
|
handleCreatePostClick = (collectionName) => {
|
||||||
@ -41,27 +46,53 @@ export default class AppHeader extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleRightIconClick = () => {
|
||||||
|
this.setState({
|
||||||
|
userMenuActive: !this.state.userMenuActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
user,
|
||||||
collections,
|
collections,
|
||||||
commands,
|
commands,
|
||||||
defaultCommands,
|
defaultCommands,
|
||||||
runCommand,
|
runCommand,
|
||||||
toggleNavDrawer,
|
toggleNavDrawer,
|
||||||
|
onLogoutClick,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { createMenuActive } = this.state;
|
|
||||||
|
const {
|
||||||
|
createMenuActive,
|
||||||
|
userMenuActive,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
fixed
|
fixed
|
||||||
theme={styles}
|
theme={styles}
|
||||||
leftIcon="menu"
|
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}
|
onLeftIconClick={toggleNavDrawer}
|
||||||
onRightIconClick={this.handleCreateButtonClick}
|
onRightIconClick={this.handleRightIconClick}
|
||||||
>
|
>
|
||||||
<IndexLink to="/">
|
<IndexLink to="/">
|
||||||
Dashboard
|
<FontIcon value="home" />
|
||||||
</IndexLink>
|
</IndexLink>
|
||||||
|
|
||||||
<FindBar
|
<FindBar
|
||||||
@ -69,9 +100,11 @@ export default class AppHeader extends React.Component {
|
|||||||
defaultCommands={defaultCommands}
|
defaultCommands={defaultCommands}
|
||||||
runCommand={runCommand}
|
runCommand={runCommand}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<IconMenu
|
||||||
|
theme={styles}
|
||||||
active={createMenuActive}
|
active={createMenuActive}
|
||||||
position="topRight"
|
icon="create"
|
||||||
|
onClick={this.handleCreateButtonClick}
|
||||||
onHide={this.handleCreateMenuHide}
|
onHide={this.handleCreateMenuHide}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@ -84,7 +117,7 @@ export default class AppHeader extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Menu>
|
</IconMenu>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { Link } from 'react-toolbox/lib/link';
|
|||||||
import { Notifs } from 'redux-notifications';
|
import { Notifs } from 'redux-notifications';
|
||||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||||
import { loadConfig } from '../actions/config';
|
import { loadConfig } from '../actions/config';
|
||||||
import { loginUser } from '../actions/auth';
|
import { loginUser, logoutUser } from '../actions/auth';
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
import {
|
import {
|
||||||
SHOW_COLLECTION,
|
SHOW_COLLECTION,
|
||||||
@ -40,6 +40,7 @@ class App extends React.Component {
|
|||||||
config: ImmutablePropTypes.map,
|
config: ImmutablePropTypes.map,
|
||||||
collections: ImmutablePropTypes.orderedMap,
|
collections: ImmutablePropTypes.orderedMap,
|
||||||
createNewEntryInCollection: PropTypes.func.isRequired,
|
createNewEntryInCollection: PropTypes.func.isRequired,
|
||||||
|
logoutUser: PropTypes.func.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
navigateToCollection: PropTypes.func.isRequired,
|
navigateToCollection: PropTypes.func.isRequired,
|
||||||
user: ImmutablePropTypes.map,
|
user: ImmutablePropTypes.map,
|
||||||
@ -138,6 +139,7 @@ class App extends React.Component {
|
|||||||
runCommand,
|
runCommand,
|
||||||
navigateToCollection,
|
navigateToCollection,
|
||||||
createNewEntryInCollection,
|
createNewEntryInCollection,
|
||||||
|
logoutUser,
|
||||||
isFetching,
|
isFetching,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -189,11 +191,13 @@ class App extends React.Component {
|
|||||||
</nav>
|
</nav>
|
||||||
</NavDrawer>
|
</NavDrawer>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
|
user={user}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
commands={commands}
|
commands={commands}
|
||||||
defaultCommands={defaultCommands}
|
defaultCommands={defaultCommands}
|
||||||
runCommand={runCommand}
|
runCommand={runCommand}
|
||||||
onCreateEntryClick={createNewEntryInCollection}
|
onCreateEntryClick={createNewEntryInCollection}
|
||||||
|
onLogoutClick={logoutUser}
|
||||||
toggleNavDrawer={this.toggleNavDrawer}
|
toggleNavDrawer={this.toggleNavDrawer}
|
||||||
/>
|
/>
|
||||||
<Panel scrollY>
|
<Panel scrollY>
|
||||||
@ -226,6 +230,9 @@ function mapDispatchToProps(dispatch) {
|
|||||||
createNewEntryInCollection: (collectionName) => {
|
createNewEntryInCollection: (collectionName) => {
|
||||||
dispatch(createNewEntryInCollection(collectionName));
|
dispatch(createNewEntryInCollection(collectionName));
|
||||||
},
|
},
|
||||||
|
logoutUser: () => {
|
||||||
|
dispatch(logoutUser());
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import expect from 'expect';
|
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import { authenticating, authenticate, authError } from '../../actions/auth';
|
import { authenticating, authenticate, authError, logout } from '../../actions/auth';
|
||||||
import auth from '../auth';
|
import auth from '../auth';
|
||||||
|
|
||||||
describe('auth', () => {
|
describe('auth', () => {
|
||||||
@ -33,8 +32,14 @@ describe('auth', () => {
|
|||||||
auth(undefined, authError(new Error('Bad credentials')))
|
auth(undefined, authError(new Error('Bad credentials')))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({
|
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 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) => {
|
const auth = (state = null, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -9,6 +9,8 @@ const auth = (state = null, action) => {
|
|||||||
return Immutable.fromJS({ user: action.payload });
|
return Immutable.fromJS({ user: action.payload });
|
||||||
case AUTH_FAILURE:
|
case AUTH_FAILURE:
|
||||||
return Immutable.Map({ error: action.payload.toString() });
|
return Immutable.Map({ error: action.payload.toString() });
|
||||||
|
case LOGOUT:
|
||||||
|
return state.remove('user');
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user