Merge pull request #58 from sharetribe/login-error-message

Authentication error messages
This commit is contained in:
Kimmo Puputti 2017-03-07 12:14:42 +02:00 committed by GitHub
commit aa0cf4330f
9 changed files with 107 additions and 42 deletions

View file

@ -7,13 +7,10 @@ const HeroSection = props => (
<div className={css.content}>
<div className={css.titleWrapper}>
<div className={css.title}>
<FormattedMessage id={'HeroSection.title'} defaultMessage={'Book Studiotime anywhere'} />
<FormattedMessage id="HeroSection.title" />
</div>
<div className={css.subTitle}>
<FormattedMessage
id={'HeroSection.subTitle'}
defaultMessage={'The largest online community to rent music studios'}
/>
<FormattedMessage id="HeroSection.subTitle" />
</div>
</div>
<div className={css.ctaWrapper}>

View file

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
import { withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { Topbar } from '../../containers';
const scrollToTop = () => {
@ -21,12 +22,32 @@ class PageLayout extends Component {
}
render() {
const { className, title, children, authError } = this.props;
// TODO: use FlashMessages for authError
const { className, title, children, authInfoError, logoutError } = this.props;
// TODO: use FlashMessages for auth errors
/* eslint-disable no-console */
if (authInfoError && console && console.error) {
console.error(authInfoError);
}
if (logoutError && console && console.error) {
console.error(logoutError);
}
/* eslint-enable no-console */
return (
<div className={className}>
<Helmet title={title} />
{authError ? <p style={{ color: 'red' }}>Error in auth: {authError.message}</p> : null}
{authInfoError
? <div style={{ color: 'red' }}>
<FormattedMessage id="PageLayout.authInfoFailed" />
</div>
: null}
{logoutError
? <div style={{ color: 'red' }}>
<FormattedMessage id="PageLayout.logoutFailed" />
</div>
: null}
<Topbar />
<h1>{title}</h1>
{children}
@ -37,17 +58,21 @@ class PageLayout extends Component {
const { any, string, instanceOf, func } = PropTypes;
PageLayout.defaultProps = { className: '', children: null, authError: null };
PageLayout.defaultProps = { className: '', children: null, authInfoError: null, logoutError: null };
PageLayout.propTypes = {
className: string,
title: string.isRequired,
children: any,
authError: instanceOf(Error),
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
// history.listen function from withRouter
listen: func.isRequired,
};
const mapStateToProps = state => ({ authError: state.Auth.error });
const mapStateToProps = state => ({
authInfoError: state.Auth.authInfoError,
logoutError: state.Auth.logoutError,
});
export default connect(mapStateToProps)(withRouter(PageLayout));

View file

@ -1,23 +1,44 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { PageLayout, NamedLink, NamedRedirect } from '../../components';
import { LoginForm, SignUpForm } from '../../containers';
import { login } from '../../ducks/Auth.duck';
export const AuthenticationPageComponent = props => {
const { location, tab, isAuthenticated, onLoginSubmit, onSignUpSubmit } = props;
const {
location,
tab,
isAuthenticated,
loginError,
onLoginSubmit,
onSignUpSubmit,
} = props;
const isLogin = tab === 'login';
const from = location.state && location.state.from ? location.state.from : null;
const authRedirect = from ? <Redirect to={from} /> : <NamedRedirect name="LandingPage" />;
// TODO: use FlashMessages for auth errors
/* eslint-disable no-console */
if (loginError && console && console.error) {
console.error(loginError);
}
/* eslint-enable no-console */
return (
<PageLayout title={`Authentication page: ${tab} tab`}>
{isAuthenticated ? authRedirect : null}
{loginError
? <div style={{ color: 'red' }}>
<FormattedMessage id="AuthenticationPage.loginFailed" />
</div>
: null}
{from
? <p>
You must log in to view the page at
<FormattedMessage id="AuthenticationPage.loginRequiredFor" />
<code>{from}</code>
</p>
: null}
@ -29,19 +50,28 @@ export const AuthenticationPageComponent = props => {
);
};
AuthenticationPageComponent.defaultProps = { tab: 'signup' };
AuthenticationPageComponent.defaultProps = {
tab: 'signup',
authInfoError: null,
loginError: null,
logoutError: null,
};
const { object, oneOf, shape, bool, func } = PropTypes;
const { object, oneOf, shape, bool, func, instanceOf } = PropTypes;
AuthenticationPageComponent.propTypes = {
location: shape({ state: object }).isRequired,
tab: oneOf(['login', 'signup']),
isAuthenticated: bool.isRequired,
loginError: instanceOf(Error),
onLoginSubmit: func.isRequired,
onSignUpSubmit: func.isRequired,
};
const mapStateToProps = state => ({ isAuthenticated: state.Auth.isAuthenticated });
const mapStateToProps = state => ({
isAuthenticated: state.Auth.isAuthenticated,
loginError: state.Auth.loginError,
});
const mapDispatchToProps = dispatch => ({
onLoginSubmit: ({ email, password }) => dispatch(login(email, password)),

View file

@ -2,7 +2,9 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="Authentication page: signup tab">
<p>
You must log in to view the page at
<FormattedMessage
id="AuthenticationPage.loginRequiredFor"
values={Object {}} />
<code>
/protected
</code>

View file

@ -6,10 +6,7 @@ import css from './HeroSearchForm.css';
const HeroSearchForm = props => {
const { className, intl, handleSubmit, pristine, submitting } = props;
const addClassName = className ? { className } : {};
const placeholderMsg = {
id: 'HeroSearchForm.placeholder',
defaultMessage: 'Location search (soon)',
};
const placeholderMsg = { id: 'HeroSearchForm.placeholder' };
return (
<form {...addClassName} onSubmit={handleSubmit}>
@ -21,7 +18,7 @@ const HeroSearchForm = props => {
placeholder={intl.formatMessage(placeholderMsg)}
/>
<button className={css.locationButton} type="submit" disabled={pristine || submitting}>
<FormattedMessage id="HeroSearchForm.search" defaultMessage="Search" />
<FormattedMessage id="HeroSearchForm.search" />
</button>
</form>
);

View file

@ -22,31 +22,37 @@ export const LOGOUT_ERROR = 'app/Auth/LOGOUT_ERROR';
// ================ Reducer ================ //
const initialState = { authInfoLoaded: false, isAuthenticated: false, error: null };
const initialState = {
authInfoLoaded: false,
isAuthenticated: false,
authInfoError: null,
loginError: null,
logoutError: null,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case AUTH_INFO_REQUEST:
return { ...state, error: null };
return { ...state, authInfoError: null };
case AUTH_INFO_SUCCESS:
return { ...state, authInfoLoaded: true, isAuthenticated: authenticated(payload) };
case AUTH_INFO_ERROR:
return { ...state, error: payload };
return { ...state, authInfoError: payload };
case LOGIN_REQUEST:
return { ...state, error: null };
return { ...state, loginError: null, logoutError: null };
case LOGIN_SUCCESS:
return { ...state, isAuthenticated: true };
case LOGIN_ERROR:
return { ...state, error: payload };
return { ...state, loginError: payload };
case LOGOUT_REQUEST:
return { ...state, error: null };
return { ...state, loginError: null, logoutError: null };
case LOGOUT_SUCCESS:
return { ...state, isAuthenticated: false };
case LOGOUT_ERROR:
return { ...state, error: payload };
return { ...state, logoutError: payload };
default:
return state;
}

View file

@ -26,7 +26,9 @@ describe('Auth duck', () => {
const state = reducer();
expect(state.authInfoLoaded).toEqual(false);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
expect(state.authInfoError).toBeNull();
expect(state.loginError).toBeNull();
expect(state.logoutError).toBeNull();
});
it('should login successfully', () => {
@ -36,23 +38,23 @@ describe('Auth duck', () => {
let state = reducer();
state = reducer(state, login(username, password));
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
expect(state.loginError).toBeNull();
state = reducer(state, loginSuccess());
expect(state.isAuthenticated).toEqual(true);
expect(state.error).toBeNull();
expect(state.loginError).toBeNull();
});
it('should handle failed login', () => {
let state = reducer();
state = reducer(state, login('username', 'pass'));
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
expect(state.loginError).toBeNull();
const error = new Error('test error');
state = reducer(state, loginError(error));
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toEqual(error);
expect(state.loginError).toEqual(error);
});
it('should set initial state for unauthenticated users', () => {
@ -62,7 +64,7 @@ describe('Auth duck', () => {
const state = reducer(initialState, authInfoSuccess(authInfoLoggedOut));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
expect(state.authInfoError).toBeNull();
});
it('should set initial state for anonymous users', () => {
@ -72,7 +74,7 @@ describe('Auth duck', () => {
const state = reducer(initialState, authInfoSuccess(authInfoAnonymous));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
expect(state.authInfoError).toBeNull();
});
it('should set initial state for unauthenticated users', () => {
@ -82,7 +84,7 @@ describe('Auth duck', () => {
const state = reducer(initialState, authInfoSuccess(authInfoLoggedIn));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(true);
expect(state.error).toBeNull();
expect(state.authInfoError).toBeNull();
});
});

View file

@ -1,8 +1,11 @@
{
"landingpage.examplelink": "Show nice studios! (from json)",
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
"AuthenticationPage.loginRequiredFor": "You must log in to view the page at",
"HeroSearchForm.placeholder": "Location search (soon)",
"HeroSearchForm.search": "Search",
"HeroSection.title": "Book Studiotime anywhere",
"HeroSection.subTitle": "The largest online community to rent music studios",
"ListingPage.loadingListingData": "Loading listing data"
"HeroSection.title": "Book Studiotime anywhere",
"ListingPage.loadingListingData": "Loading listing data",
"PageLayout.authInfoFailed": "Could not get authentication information.",
"PageLayout.logoutFailed": "Logout failed. Please try again."
}

View file

@ -2,17 +2,20 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import { IntlProvider } from 'react-intl';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import configureStore from '../store';
import localeData from '../translations/en.json';
// Provide all the context for components that connect to the Redux
// store, i18n, router, etc.
export const TestProvider = props => {
const store = configureStore();
addLocaleData([...en]);
return (
<IntlProvider locale="en">
<IntlProvider locale="en" messages={localeData}>
<BrowserRouter>
<Provider store={store}>
{props.children}