Add user logout and user login error handling
This commit is contained in:
parent
3701917869
commit
6bfcfa14b9
@ -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": {
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
19
src/public/js/components/errors.jsx
Normal file
19
src/public/js/components/errors.jsx
Normal 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>×</span>
|
||||||
|
</button>
|
||||||
|
<p>{props.errors.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
14
src/public/js/reducers/errors.js
Normal file
14
src/public/js/reducers/errors.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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`) {
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user