Add user logout and user login error handling

This commit is contained in:
Grégoire Delattre 2016-11-16 01:54:01 +01:00
parent 3701917869
commit 6bfcfa14b9
11 changed files with 131 additions and 35 deletions

View File

@ -13,6 +13,7 @@
"babel-polyfill": "^6.16.0", "babel-polyfill": "^6.16.0",
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"history": "^4.4.0",
"jquery": "^2.2.4", "jquery": "^2.2.4",
"jwt-decode": "^2.1.0", "jwt-decode": "^2.1.0",
"react": "^15.3.2", "react": "^15.3.2",
@ -23,7 +24,6 @@
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-auth-wrapper": "^0.9.0", "redux-auth-wrapper": "^0.9.0",
"redux-logger": "^2.7.4", "redux-logger": "^2.7.4",
"redux-promise-middleware": "^4.1.0",
"redux-thunk": "^2.1.0" "redux-thunk": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,8 +1,7 @@
export const ADD_MOVIES = 'ADD_MOVIES' import axios from 'axios'
export const SELECT_MOVIE = 'SELECT_MOVIE'
export const IS_USER_LOGGED_IN = 'IS_USER_LOGGED_IN'
// Select Movie // Select Movie
export const SELECT_MOVIE = 'SELECT_MOVIE'
export function selectMovie(index) { export function selectMovie(index) {
return { return {
type: SELECT_MOVIE, type: SELECT_MOVIE,
@ -10,6 +9,7 @@ export function selectMovie(index) {
} }
} }
export const ADD_MOVIES = 'ADD_MOVIES'
export function addMovies(movies) { export function addMovies(movies) {
return { return {
type: ADD_MOVIES, type: ADD_MOVIES,
@ -17,8 +17,66 @@ export function addMovies(movies) {
} }
} }
export const IS_USER_LOGGED_IN = 'IS_USER_LOGGED_IN'
export function isUserLoggedIn() { export function isUserLoggedIn() {
return { return {
type: IS_USER_LOGGED_IN, type: IS_USER_LOGGED_IN,
} }
} }
export const ADD_ERROR = 'ADD_ERROR'
export function addError(message) {
return {
type: ADD_ERROR,
payload: {
message,
}
}
}
export const DISMISS_ERROR = 'DISMISS_ERROR'
export function dismissError() {
return {
type: DISMISS_ERROR,
}
}
export const USER_LOGOUT = 'USER_LOGOUT'
export function userLogout() {
return {
type: USER_LOGOUT,
}
}
export const USER_LOGIN_FULFILLED = 'USER_LOGIN_FULFILLED',
USER_LOGIN_PENDING = 'USER_LOGIN_PENDING';
export function loginUser(username, password) {
return function(dispatch) {
dispatch({
type: USER_LOGIN_PENDING,
})
axios.post(
'/users/login',
{
username: username,
password: password,
},
).then(response => {
if (response.data && response.data.type && response.data.type === 'error')
{
dispatch({
type: ADD_ERROR,
payload: {
message: response.data.message,
}
})
}
dispatch({
type: USER_LOGIN_FULFILLED,
payload: response,
})
}).catch(error => {
console.log(error)
})
}
}

View File

@ -19,6 +19,7 @@ import store, { history } from './store'
import NavBar from './components/navbar.jsx' import NavBar from './components/navbar.jsx'
import MovieList from './components/movie-list.jsx' import MovieList from './components/movie-list.jsx'
import UserLoginForm from './components/user-login.jsx' import UserLoginForm from './components/user-login.jsx'
import Error from './components/errors.jsx'
class Main extends React.Component { class Main extends React.Component {
componentWillMount() { componentWillMount() {
@ -30,6 +31,7 @@ class Main extends React.Component {
<div className="navbar navbar-inverse navbar-fixed-top"> <div className="navbar navbar-inverse navbar-fixed-top">
<NavBar {...this.props}/> <NavBar {...this.props}/>
</div> </div>
<Error {...this.props}/>
<div className="container-fluid"> <div className="container-fluid">
<div className="container"> <div className="container">
{React.cloneElement(this.props.children, this.props)} {React.cloneElement(this.props.children, this.props)}
@ -44,6 +46,7 @@ function mapStateToProps(state) {
return { return {
movieStore: state.movieStore, movieStore: state.movieStore,
userStore: state.userStore, userStore: state.userStore,
errors: state.errors,
} }
} }
@ -61,7 +64,6 @@ const UserIsAuthenticated = UserAuthWrapper({
predicate: user => user.isLogged, predicate: user => user.isLogged,
failureRedirectPath: '/users/login', failureRedirectPath: '/users/login',
}) })
const Authenticated = UserIsAuthenticated((props) => props.children);
ReactDOM.render(( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
@ -69,9 +71,7 @@ ReactDOM.render((
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRedirect to="/users/login" /> <IndexRedirect to="/users/login" />
<Route path="/users/login" component={UserLoginForm} /> <Route path="/users/login" component={UserLoginForm} />
<Route component={Authenticated}> <Route path="/movies/popular" component={UserIsAuthenticated(MovieList)} />
<Route path="/movies/popular" component={MovieList} />
</Route>
</Route> </Route>
</Router> </Router>
</Provider> </Provider>

View File

@ -0,0 +1,19 @@
import React from 'react'
export default function Error(props) {
if (!props.errors.message) {
return null
}
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<div className="alert alert-warning">
<button type="button" className="close" onClick={props.dismissError}>
<span>&times;</span>
</button>
<p>{props.errors.message}</p>
</div>
</div>
</div>
)
}

View File

@ -30,7 +30,7 @@ export default class NavBar extends React.Component {
<li><Link to="/users/login">Login</Link></li> <li><Link to="/users/login">Login</Link></li>
} }
{isLoggedIn && {isLoggedIn &&
<li><Link to="/users/logout">Logout</Link></li> <li><a onClick={this.props.userLogout}>Logout</a></li>
} }
</ul> </ul>
</div> </div>

View File

@ -1,6 +1,4 @@
import React from 'react' import React from 'react'
import axios from 'axios'
import store from '../store'
export default class UserLoginForm extends React.Component { export default class UserLoginForm extends React.Component {
constructor(props) { constructor(props) {
@ -14,16 +12,7 @@ export default class UserLoginForm extends React.Component {
} }
const username = this.refs.username.value; const username = this.refs.username.value;
const password = this.refs.password.value; const password = this.refs.password.value;
store.dispatch({ this.props.loginUser(username, password);
type: "USER_LOGIN",
payload: axios.post(
'/users/login',
{
username: username,
password: password,
},
)
})
} }
render() { render() {
return ( return (

View File

@ -0,0 +1,14 @@
import { ADD_ERROR, DISMISS_ERROR } from '../actions/actionCreators'
export default function error(state = {}, action) {
switch (action.type) {
case ADD_ERROR:
return Object.assign({}, state, {
message: action.payload.message,
})
case DISMISS_ERROR:
return {};
default:
return state;
}
}

View File

@ -3,11 +3,13 @@ import { routerReducer } from 'react-router-redux'
import movieStore from './movie-store' import movieStore from './movie-store'
import userStore from './users' import userStore from './users'
import errors from './errors'
const rootReducer = combineReducers({ const rootReducer = combineReducers({
routing: routerReducer, routing: routerReducer,
movieStore, movieStore,
userStore userStore,
errors,
}) })
export default rootReducer; export default rootReducer;

View File

@ -1,4 +1,4 @@
import { IS_USER_LOGGED_IN } from '../actions/actionCreators' import { IS_USER_LOGGED_IN, USER_LOGIN_PENDING, USER_LOGIN_FULFILLED, USER_LOGOUT } from '../actions/actionCreators'
import jwtDecode from 'jwt-decode' import jwtDecode from 'jwt-decode'
const defaultState = { const defaultState = {
@ -8,25 +8,26 @@ const defaultState = {
isLogged: false, isLogged: false,
}; };
// This actions are generated from the promise middleware
export default function userStore(state = defaultState, action) { export default function userStore(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case 'USER_LOGIN_PENDING': case USER_LOGIN_PENDING:
return Object.assign({}, state, { return Object.assign({}, state, {
userLoading: true, userLoading: true,
}) })
case 'USER_LOGIN_FULFILLED': case USER_LOGIN_FULFILLED:
if (action.payload.data.type === "error") { const data = action.payload.data;
if (data && data.type === "error") {
return logoutUser(state) return logoutUser(state)
} }
return updateFromToken(state, action.payload.data.token) return updateFromToken(state, data.token)
case IS_USER_LOGGED_IN: case IS_USER_LOGGED_IN:
let localToken = localStorage.getItem('token'); let localToken = localStorage.getItem('token');
if (!localToken || localToken === "") { if (!localToken || localToken === "") {
return state; return state;
} }
return updateFromToken(state, localToken) return updateFromToken(state, localToken)
case USER_LOGOUT:
return logoutUser(state)
default: default:
return state; return state;
} }

View File

@ -2,7 +2,6 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { syncHistoryWithStore } from 'react-router-redux' import { syncHistoryWithStore } from 'react-router-redux'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import promise from 'redux-promise-middleware'
import { routerMiddleware } from 'react-router-redux' import { routerMiddleware } from 'react-router-redux'
// Import the root reducer // Import the root reducer
@ -10,7 +9,7 @@ import rootReducer from './reducers/index'
const routingMiddleware = routerMiddleware(hashHistory) const routingMiddleware = routerMiddleware(hashHistory)
const middlewares = [promise(), thunk, routingMiddleware]; const middlewares = [thunk, routingMiddleware];
// Only use in development mode (set in webpack) // Only use in development mode (set in webpack)
if (process.env.NODE_ENV === `development`) { if (process.env.NODE_ENV === `development`) {

View File

@ -1568,6 +1568,16 @@ hawk@~3.1.3:
hoek "2.x.x" hoek "2.x.x"
sntp "1.x.x" sntp "1.x.x"
history:
version "4.4.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.4.0.tgz#b1369588cb9e5d80219d0b1f866b0ac62c14a7f8"
dependencies:
invariant "^2.2.1"
loose-envify "^1.2.0"
resolve-pathname "^2.0.0"
value-equal "^0.1.1"
warning "^3.0.0"
history@^3.0.0: history@^3.0.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/history/-/history-3.2.1.tgz#71c7497f4e6090363d19a6713bb52a1bfcdd99aa" resolved "https://registry.yarnpkg.com/history/-/history-3.2.1.tgz#71c7497f4e6090363d19a6713bb52a1bfcdd99aa"
@ -2716,10 +2726,6 @@ redux-logger:
dependencies: dependencies:
deep-diff "0.3.4" deep-diff "0.3.4"
redux-promise-middleware:
version "4.1.0"
resolved "https://registry.yarnpkg.com/redux-promise-middleware/-/redux-promise-middleware-4.1.0.tgz#8477866fa09837c1f08f5869c473747577f5446a"
redux-thunk: redux-thunk:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98"
@ -2807,6 +2813,10 @@ resolve-dir@^0.1.0:
expand-tilde "^1.2.2" expand-tilde "^1.2.2"
global-modules "^0.2.3" global-modules "^0.2.3"
resolve-pathname@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.0.2.tgz#e55c016eb2e9df1de98e85002282bfb38c630436"
resolve@^1.1.6, resolve@^1.1.7: resolve@^1.1.6, resolve@^1.1.7:
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
@ -3144,6 +3154,10 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0" spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0" spdx-expression-parse "~1.0.0"
value-equal@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.1.1.tgz#b174df21f203c81e17f2e4d59d3a900024cbef7b"
verror@1.3.6: verror@1.3.6:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"