Пишем авторизацию на React Redux

Пишем авторизацию на React Redux

Структура проекта:

Файл public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href ="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
    <title>Список задач</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Файл src/index.js

import React from 'react';
import {Provider} from 'react-redux';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
import configureStore from './store';

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, 
  document.getElementById('root'))
;

if (module.hot) {
  module.hot.accept('./components/App', () => {
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>, 
      document.getElementById('root')
    )
  })
}

serviceWorker.unregister();

Файл src/actions.auth.js

import * as types from '../constants/auth';
import callApi from '../utils/call-api';

export function signup(email, password, repeatPassword){
  return (dispatch) => {
    dispatch({
      type: types.SIGNUP_REQUEST
    })
    return callApi('/signup', undefined, {method: "POST"},{
      email,
      password,
      repeatPassword
    })
      .then(json => {
        if (!json.token) {
          throw new Error('Джок токен!');
        }      
        localStorage.setItem('token', json.token);         
        dispatch({
          type: types.SIGNUP_SUCCESS,
          payload: json
        })  
      })
      .catch(reason => dispatch({
        type: types.SIGNUP_FAILURE,
        payload: reason
      }));
  };
}

export function login(username, password){
  return (dispatch) => {
    dispatch({
      type: types.LOGIN_REQUEST
    })
    return callApi('/login', undefined, {method: "POST"}, {
      username,
      password,
    })     
      .then(json => {
        if (!json.token) {
          throw new Error('Джок токен!');
        }      
        localStorage.setItem('token', json.token);            
        
        dispatch({
          type: types.LOGIN_SUCCESS,
          payload: json
        })  
      })
      .catch(reason => dispatch({
        type: types.LOGIN_FAILURE,
        payload: reason
      }));
  };
}

export function logout(){
  return (dispatch) => {
    dispatch({
      type: types.LOGOUT_REQUEST
    })
  }; 
}

export function recieveAuth() {
  return(dispatch, getState) => {
    const {token} = getState().auth;
    if (!token) {
      dispatch({
        type: types.RECIEVE_AUTH_FAILURE
      })     
    }
    return callApi('/userauth', token)
      .then(json => {       
        dispatch({
          type: types.RECIEVE_AUTH_SUCCESS,
          payload: json
        })  
      })
      .catch(reason => dispatch({
        type: types.RECIEVE_AUTH_FAILURE,
        payload: reason
      }));
  }
}

Файл src/auth/index.js

export * from './auth';
export * from './todo';
export * from './services';

Файл src/auth/services.js

import * as types from '../constants';
import history from '../utils/history';

export function redirect(to) {
  return (dispatch) => {
    history.push(`${process.env.PUBLIC_URL}${to}`);
    dispatch({
      type: types.REDIRECT,
      payload: {to}
    })
  }
}

Файл src/auth/todo.js

import * as types from '../constants/todo';
import callApi from '../utils/call-api';

export function fetchTodo() {
  return (dispatch, getState) => {
    const {token} = getState().auth;

    dispatch({
      type: types.FETCH_TODO_REQUEST
    })
    return callApi('/todo', token)
      .then(data => dispatch({
        type: types.FETCH_TODO_SUCCESS,
        payload: data
      }))
      .catch(reason => dispatch({
        type: types.FETCH_TODO_FAILURE,
        payload: reason
      }))
  };
}

Файл src/components/App.js

import React from 'react';
import {Router, Route, Switch, Redirect} from 'react-router-dom';
import PrivateRoute from '../containers/PrivateRoute';
import TodoPage from '../containers/TodoPage';
import WelcomePage from '../containers/WelcomePage';
import history from '../utils/history';

const App = () =>(
  <Router history={history}>
    <Switch>
      <Route exact path='/(welcome)?' component={WelcomePage} />
      <PrivateRoute path='/todo' component={TodoPage} />
      <Redirect to='/' />
    </Switch>
  </Router>
);

export default App;

Файл src/components/LoginForm.js

import React from 'react';

class LoginForm extends React.Component {
  state = {
    email: {
      value: ''
    },
    password: {
      value: ''
    }  
  }

  handleInputChange = (event) => {
    event.persist();
    const { name, value } = event.target;
    this.setState((prevState)=>({
      [name]: {
        ...prevState[name],
        value
      }
    }))
  }


  handleSubmit = (event) => {
    event.preventDefault();
    const { email, password } = this.state;
    this.props.onSubmit(email.value, password.value);
  }

  render() {
    const { email, password} = this.state;
    return(
      <form onSubmit={this.handleSubmit}>
        <div className="form-group">
          <label>Е-майл</label>
          <input type="email" value={email.value} onChange={this.handleInputChange} className="form-control" name="email" aria-describedby="emailHelp" placeholder="Введите е-майл" />
        </div>
        <div className="form-group">
          <label>Пароль</label>
          <input type="password" value={password.value} onChange={this.handleInputChange} className="form-control" name="password" placeholder="Введите пароль" />
        </div>
        <button type="submit" className="btn btn-primary">Войти</button>
      </form>
    )
  }
}

export default LoginForm;

Файл src/components/SignupForm.js

import React from 'react';

class SignupForm extends React.Component {
  state = {
    email: {
      value: ''
    },
    password: {
      value: ''
    },
    repeatPassword: {
      value: ''
    }  
  }

  handleInputChange = (event) => {
    event.persist();
    const { name, value } = event.target;
    this.setState((prevState)=>({
      [name]: {
        ...prevState[name],
        value
      }
    }))
  }

  handleSubmit = (event) => {
    event.preventDefault();
    const { email, password, repeatPassword } = this.state;
    this.props.onSubmit(email.value, password.value, repeatPassword.value);
  }

  render() {
    const { email, password, repeatPassword} = this.state;
    return(
      <form onSubmit={this.handleSubmit}>
        <div className="form-group">
          <label>Е-майл</label>
          <input type="email" value={email.value} onChange={this.handleInputChange} className="form-control" name="email" aria-describedby="emailHelp" placeholder="Введите е-майл" />
        </div>
        <div className="form-group">
          <label>Пароль</label>
          <input type="password" value={password.value} onChange={this.handleInputChange} className="form-control" name="password" placeholder="Введите пароль" />
        </div>
        <div className="form-group">
          <label>Повторите пароль</label>
          <input type="password" value={repeatPassword.value} onChange={this.handleInputChange} className="form-control" name="repeatPassword" placeholder="Введите пароль" />
        </div>

        <button type="submit" className="btn btn-primary">Зарегистрироваться</button>
      </form>
    )
  }
}

export default SignupForm;

Файл src/components/TodoPage.js

import React from 'react';

class TodoPage extends React.Component {
  componentDidMount(){
    const { fetchTodo } = this.props;
    Promise.all([
      fetchTodo(),
    ])
  }
  render() {
    const { todos } = this.props;
    const elements = todos.map((item)=>{
      const{_id, name} = item;
      return(
        <li key={_id} className="list-group-item">{name}</li>
      )
    })
    return (
      <div>
        <h1>Список задач</h1>
        <ul className="list-group todo-list">
          {elements}
        </ul>
      </div> 
    )
  }
}

export default TodoPage;

Файл src/components/WelcomePage.js

import React from 'react';
import {Redirect} from 'react-router-dom';
import LoginForm from './LoginForm';
import SignupForm from './SignupForm';

class WelcomePage extends React.Component {
  state = {
    vhod: true
  }

  onSubmitVhod = (event) => {
    event.persist();
    this.setState((prevState) => ({
      vhod: true
    }))
  }

  onSubmitRegister = (event) => {
    event.persist();
    this.setState((prevState) => ({
      vhod: false
    }))
  } 

  render() {
    const {signup, login, isAuthenticated} = this.props;

    let classVhod = 'nav-link';
    let classRegister = 'nav-link';
    if (this.state.vhod) {
      classVhod = classVhod + ' active';
    } else {
      classRegister = classRegister + ' active';
    }

    if (isAuthenticated){
      return (
        <Redirect to="/todo" />
      )  
    }
    return (
      <div className="card text-center">
        <div className="card-header">
          <ul className="nav nav-tabs card-header-tabs">
            <li className="nav-item" onClick={this.onSubmitVhod}>
              <p className={classVhod}>Вход</p>
            </li>
            <li className="nav-item" onClick={this.onSubmitRegister}>
              <p className={classRegister}>Регитрация</p>
            </li>
          </ul>
        </div>
        <div className="card-body">
          {this.state.vhod ? (
            <LoginForm onSubmit={login} />  
           ) : (
            <SignupForm onSubmit={signup} />
           ) 
          }
        </div>
      </div>
    )
  }
}

export default WelcomePage;

Файл src/constants/auth.js

export const SIGNUP_REQUEST = Symbol('auth/SIGNUP_REQUEST');
export const SIGNUP_SUCCESS = Symbol('auth/SIGNUP_SUCCESS');
export const SIGNUP_FAILURE = Symbol('auth/SIGNUP_FAILURE');

export const LOGIN_REQUEST = Symbol('auth/LOGIN_REQUEST');
export const LOGIN_SUCCESS = Symbol('auth/LOGIN_SUCCESS');
export const LOGIN_FAILURE = Symbol('auth/LOGIN_FAILURE');

export const LOGOUT_REQUEST = Symbol('auth/LOGOUT_REQUEST');
export const LOGOUT_SUCCESS = Symbol('auth/LOGOUT_SUCCESS');
export const LOGOUT_FAILURE = Symbol('auth/LOGOUT_FAILURE');

export const RECIEVE_AUTH_REQUEST = Symbol('auth/RECIEVE_AUTH_REQUEST');
export const RECIEVE_AUTH_SUCCESS = Symbol('auth/RECIEVE_AUTH_SUCCESS');
export const RECIEVE_AUTH_FAILURE = Symbol('auth/RECIEVE_AUTH_FAILURE');

Файл src/constants/index.js

export * from './auth';
export * from './services';

Файл src/constants/services.js

export const REDIRECT = Symbol('services/REDIRECT');

Файл src/constants/todo.js

export const FETCH_TODO_REQUEST = Symbol('todo/FETCH_TODO_REQUEST');
export const FETCH_TODO_SUCCESS = Symbol('todo/FETCH_TODO_SUCCESS');
export const FETCH_TODO_FAILURE = Symbol('todo/FETCH_TODO_FAILURE');

Файл src/containets/PrivateRoute.js

import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Route, Redirect, withRouter} from 'react-router-dom';
import {recieveAuth} from '../actions';

class PrivateRoute extends React.Component {
  componentDidMount() {
    this.props.recieveAuth();
  }
  render() {
    const { component: Component, isAuthenticated, ...rest } = this.props;
    return (
      <Route {...rest} render={props => (
        isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect to={{
            state: {from : props.location}
          }} />
        )
      )} />
    );
  }  
}

const mapStateToProps = state => ({
  isAuthenticated: state.auth.isAuthenticated,
});

const mapDispatchToProps = dispatch => bindActionCreators({
  recieveAuth
}, dispatch);

export default withRouter(connect(
  mapStateToProps,
  mapDispatchToProps
)(PrivateRoute));

Файл src/containers/TodoPage.js

import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {fetchTodo} from '../actions/todo';
import * as fromTodos from '../reducers/todo';
import TodoPage from '../components/TodoPage';

const mapStateToProps = state => ({
  todos: fromTodos.getByIds(state.todo, state.todo.todoIds)
});

const mapDispatchToProps = dispatch => bindActionCreators({
  fetchTodo
}, dispatch);

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoPage);

Файл src/containers/WelcomePage.js

import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import { signup, login } from '../actions';
import WelcomePage from '../components/WelcomePage';

const mapStateToProps = state => ({
  isAuthenticated: state.auth.isAuthenticated,
});

const mapDispatchToProps = dispatch => bindActionCreators({
  signup,
  login,  
}, dispatch);

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(WelcomePage);

Файл src/reducers/auth.js

import * as types from '../constants';

const token = localStorage.getItem('token');

const initialState = {
  isAuthenticated: !!token,
  user: null,
  token,
};

export default function auth(state = initialState, action) {
  switch(action.type){
    case types.SIGNUP_SUCCESS:
    case types.LOGIN_SUCCESS:
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload.user,
        token: action.payload.token
      };
    case types.RECIEVE_AUTH_SUCCESS:
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload.user,
        
      }
    case types.SIGNUP_FAILURE:
    case types.LOGIN_FAILURE:
    case types.RECIEVE_AUTH_FAILURE:
    case types.LOGOUT_SUCCESS:
      return {
        ...state,
        isAuthenticated: false,
        user: null,
        token: '',
      };      
    default:
      return state;  
  }
}

Файл src/reducers/index.js

import {combineReducers} from 'redux';
import auth from './auth';
import todo from './todo';

export default combineReducers({
  auth,
  todo
});

Файл src/reducers/todo.js

import {combineReducers} from 'redux';
import * as types from '../constants/todo';

const initialState = {
  todoIds: [],
  byIds: {}
};

const todoIds = (state = initialState.todoIds, action) => {
  switch (action.type) {
    case types.FETCH_TODO_SUCCESS:
      return action.payload.todo.map(getTodoId);
    default:
      return state;
  }
}

const byIds = (state = initialState.byIds, action) => {
  switch (action.type) {
    case types.FETCH_TODO_SUCCESS:
      return {
        ...state,
        ...action.payload.todo.reduce((ids, todo) => ({
          ...ids,
          [todo._id] : todo
        }), {}),
      }
    default:
      return state;
  }
}

export default combineReducers({
  todoIds,
  byIds
})

export const getTodoId = (todo) => todo._id;
export const getByIds = (state, ids) => ids.map(id => state.byIds[id]);

Файл src/store/index.js

import {createStore, applyMiddleware, compose} from 'redux';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import rootReducer from '../reducers';

export default function configureStore() {
  if (process.env.NODE_ENV === 'production') {
    return createStore(
      rootReducer,
      applyMiddleware(
        thunkMiddleware
      )
    )
  } else {
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({serialize: true}) : compose;
    const store = createStore(
      rootReducer,
      composeEnhancers(
        applyMiddleware(
          thunkMiddleware,
          loggerMiddleware
        )   
      )
    )
    if (module.hot) {
      module.hot.accept('../reducers', ()=>{
        store.replaceReducer(rootReducer)
      })
    }
    return store;
  }
}

Файл src/utils/call-api.js

import fetch from 'isomorphic-fetch';

export default function callApi(endpoint, token, options, payload) {
  const authHeaders = token ? {
    'Authorization' : `Bearer ${token}`  
  } : {};
  return fetch(`http://localhost:5000${endpoint}`, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'Content-type': 'application/json',
      ...authHeaders
    },
    body: JSON.stringify(payload),
    ...options
  })
    .then(response => response.json())
    .then(json => {
      if (json.success) {
        return json;
      }
      throw new Error(json.message);
    })     
}

Файл src/utils/history.js

import {createBrowserHistory} from 'history';

export default createBrowserHistory();