LoginSignup
0
0

More than 3 years have passed since last update.

[MERN⑦] React User Authentication

Last updated at Posted at 2020-12-02

~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. Auth Reducer & Register Action

① TYPEを追加

actions/types.js
export const SET_ALERT = 'SET_ALERT';
export const REMOVE_ALERT = 'REMOVE_ALERT';
+ export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
+ export const REGISTER_FAIL = 'REGISTER_FAIL';

② Auth Reducer
~~~ 詳細説明 ~~~
(1) localStorageは、cookieのようにデータをブラウザで永続的に保存できる仕組み。デフォルトでtokenを取得する、認証成功したらlocalStorageにtokenデータを保存する。認証失敗したらtokenを削除。
(2) 認証したら、null->trueに変わる。他でも認証判定でisAuthenticatedを使用する。
(3) 認証試みる直前はLoadingする設定。認証成功または失敗したらLoading終わる->false。
(4) 認証前はname,email,avatarはnullの状態にする。
(5) Auth

reducers/auth.js
import { REGISTER_SUCCESS, REGISTER_FAIL } from '../actions/types';

const initialState = {
(1)  token: localStorage.getItem('token'),
(2)  isAuthenticated: null,
(3)  loading: true,
(4)  user: null,
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case REGISTER_SUCCESS:
(1)  localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
(2)     isAuthenticated: true,
(3)     loading: false,
      };
    case REGISTER_FAIL:
(1)   localStorage.removeItem('token');
      return {
        ...state,
(1)     token: null,
(2)     isAuthenticated: false,
(3)     loading: false,
      };
    default:
      return state;
  }
}

③ reducers/index.jsにauthを追加

reducers/index.js
import { combineReducers } from 'redux';
import alert from './alert';
+ import auth from './auth';

export default combineReducers({
  alert,
+ auth,
});

④ Authアクション
~~~ 詳細説明 ~~~
(1)認証成功-> name,email,passowrdをJSON形式でAPIにPOST -> REGISTER_SUCCESSを発火。
*stringify=JSON形式に変換する
(2)payload:res.data これはtoken
(3)認証失敗->setAlertアクション->REGITER_FAILを発火
*payloadは不要(auth reducerのREGITER_FAILにpayloadはない。)
*array.forEach(element => {}); (arrayはerrors、elementは今回対象のエラー(要は認証失敗エラー))

actions/auth.js
import axios from 'axios';
import { setAlert } from './alert';
import { REGISTER_SUCCESS, REGISTER_FAIL } from './types';

// Register User
export const register = ({ name, email, password }) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const body = JSON.stringify({ name, email, password });

(1)  try {
    const res = await axios.post('/api/users', body, config);

    dispatch({
      type: REGISTER_SUCCESS,
(2)   payload: res.data, //token
    });
 } catch (err) {
    const errors = err.response.data.errors;

(3) if (errors) {
      errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
    }
    dispatch({
      type: REGISTER_FAIL,
    });
  }
};

④ Registerコンポーネントにregister関数を設置。

components/auth/Register.js
...
import { setAlert } from '../../actions/alert';
+ import { register } from '../../actions/auth';
import PropTypes from 'prop-types';

+ const Register = ({ setAlert, register }) => {
...
  const onSubmit = async (e) => {
    e.preventDefault();
    if (password !== password2) {
      setAlert('Passwords do not match', 'danger');
    } else {
+     register({ name, email, password });
    }
  };
  return (
...
Register.propTypes = {
  setAlert: PropTypes.func.isRequired,
+ register: PropTypes.func.isRequired,
};

+ export default connect(null, { setAlert, register })(Register);

2. Load User & Set Auth Token

① ユーティリティフォルダ生成 -> setAuthTokenファイルを生成。

src/utils/setAuthToken.js
import axios from 'axios';

const setAuthToken = (token) => {
  if (token) {
    axios.defaults.headers.common['x-auth-token'] = token;
  } else {
    delete axios.defaults.headers.common['x-auth-token'];
  }
};
export default setAuthToken;

② TYPEを追加。

actions/type.js
...
+ export const USER_LOADED = 'USER_LOADED';
+ export const AUTH_ERROR = 'AUTH_ERROR';

③ Load Userアクションを追加

actions/auth.js
import axios from 'axios';
import { setAlert } from './alert';
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
+  USER_LOADED,
+  AUTH_ERROR,
} from './types';
+ import setAuthToken from '../utils/setAuthToken';

以下全て追加
// Load User
export const loadUser = () => async (dispatch) => {
  if (localStorage.token) {
    try {
      setAuthToken(localStorage.token);
      const res = await axios.get('/api/auth');
      dispatch({
        type: USER_LOADED,
        payload: res.data,
      });
    } catch (err) {
      dispatch({
        type: AUTH_ERROR,
      });
    }
  } else {
    dispatch({
      type: AUTH_ERROR,
    });
  }
};

// Register User
...

④ Auth ReducerにUSER_LOADEDとAUTH_ERRORを設置。

reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
+  USER_LOADED,
+  AUTH_ERROR,
} from '../actions/types';

const initialState = {
  token: localStorage.getItem('token'),
  isAuthenticated: null,
  loading: true,
  user: null,
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
+   case USER_LOADED:
+     return {
+       ...state,
+       isAuthenticated: true,
+       loading: false,
+       user: payload, //name,email,avatar etc.
+     };
    case REGISTER_SUCCESS:
      localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
        isAuthenticated: true,
        loading: false,
      };
    case REGISTER_FAIL:
+   case AUTH_ERROR:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
      };
    default:
      return state;
  }
}

⑤ ユーザーがサイトをロードした時にいつも行う動作

ユーザーがサイトをロードした時...
(1) Tokenの有無確認。あればlocalStorageにセット。
(2) loadUserをマウント(userEffect)する。
(3) 「空の配列 ([]) を渡した場合、副作用内では props と state の値は常にその初期値のままになる。」
https://ja.reactjs.org/docs/hooks-effect.html

src/App.js
+ import React, { Fragment, useEffect } from 'react';
...
//Redux
import { Provider } from 'react-redux';
import store from './store';
+ import { loadUser } from './actions/auth';
+ import setAuthToken from './utils/setAuthToken';
import './App.css';

(2)  useEffect(() => {
(1)    setAuthToken(localStorage.token);
(2)    store.dispatch(loadUser());
(3)  }, []);

+   return (
    <Provider store={store}>
...
    </Provider>
  );
+ };
export default App;

⑥ Authアクションのregister関数が成功した場合に、loadUserを発火する。

actions/auth.js
...
// Register User
export const register = ({ name, email, password }) => 
...
  try {
    const res = await axios.post('/api/users', body, config);

    dispatch({
      type: REGISTER_SUCCESS,
      payload: res.data, //token
    });

+    dispatch(loadUser());
  } catch (err) {
...

3. User Login

① アクションにType追加。

actions/type.js
...
+ export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+ export const LOGIN_FAIL = 'LOGIN_FAIL';

register関数をコピペして、login関数を記述。

actions/auth.js
...
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
+  LOGIN_SUCCESS,
+  LOGIN_FAIL,
} from './types';
...
//以下Register関数をコピペ
// Login User
+ export const login = ({ email, password }) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

+  const body = JSON.stringify({ email, password });

  try {
    const res = await axios.post('/api/auth', body, config);

    dispatch({
+      type: LOGIN_SUCCESS,
      payload: res.data, //token
    });

    dispatch(loadUser());
  } catch (err) {
    const errors = err.response.data.errors;

    if (errors) {
      errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
    }
    dispatch({
+      type: LOGIN_FAIL,
    });
  }
};

③ AuthReducerにLOGIN_SUCCESSと_FAILを追加。

reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
+  LOGIN_SUCCESS,
+  LOGIN_FAIL,
} from '../actions/types';

...
    case REGISTER_SUCCESS:
+    case LOGIN_SUCCESS:
      localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
        isAuthenticated: true,
        loading: false,
      };
    case REGISTER_FAIL:
    case AUTH_ERROR:
+    case LOGIN_FAIL:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
      };
    default:
      return state;
  }
}

④ 認証成功時のRedirect
以下、components/auth/Register.jsも同様に追加

components/auth/Login.js
import React, { Fragment, useState } from 'react';
+ import { Link, Redirect } from 'react-router-dom';
...
+const Login = ({ login, isAuthenticated }) => {
...
+  // Redirect if logged in
+  if (isAuthenticated) {
+    return <Redirect to="/dashboard" />;
+  }
  return (
...
Login.propTypes = {
  login: PropTypes.func.isRequired,
+  isAuthenticated: PropTypes.bool,
};

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

+ export default connect(mapStateToProps, { login })(Login);

4. Logout & Navbar Links

① TYPEアクションに追加。

actions/types.js
+ export const LOGOUT = 'LOGOUT';

② アクションとリデューサーにLOGOUTを追加。

actions/auth.js
//...
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
+  LOGOUT,
} from './types';
//...
+ // Logout / Clear Profile
+ export const logout = () => (dispatch) => {
+   dispatch({ type: LOGOUT });
+ };
reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
+  LOGOUT,
} from '../actions/types';
//...
    case REGISTER_FAIL:
    case AUTH_ERROR:
    case LOGIN_FAIL:
+    case LOGOUT:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
        user: null,
      };
    default:
      return state;
  }
}

③ Navbarの認証
(1) href="#!" <-シャープとエクスクラメーションマークの順番に注意。
(2) Loadingしていない場合、認証済ならauthLinks、未認証ならguestLinks
 ・{!loading && ''}の説明-> 「if not loading, then do ''」
 ・{XXX ? YYY : ZZZ}の説明-> 「if XXXX, YYY else ZZZ」
!loadingの疑問とその解決)
 initialState(reducers/auth.js)がloading: trueなので、authLinksguestLinksがNavbarに出てこないと思いきや、App.jsでloadUserを発火(USER_LOADED or AUTH_ERROR)しているので、いづれにしてもloading:falseとなる。

components/layout/Navbar.js
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { logout } from '../../actions/auth';

 const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => {
   const authLinks = (
     <ul>
       <li>
(1)      <a onClick={logout} href="#!">
           <i className="fas fa-sign-out-alt" />{' '}
          <span className="hide-sm">Logout</span>
        </a>
      </li>
    </ul>
  );

  const guestLinks = (
    <ul>
      <li>
        <a href="#!">Developers</a>
      </li>
      <li>
        <Link to="/register">Register</Link>
      </li>
      <li>
        <Link to="/login">Login</Link>
      </li>
    </ul>
  );

  return (
    <nav className="navbar bg-dark">
      <h1>
        <Link to="/">
          <i className="fas fa-code" /> Refnote
        </Link>
      </h1>
(2)   {!loading && (
        <Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
      )}
    </nav>
  );
};

Navbar.propTypes = {
  logout: PropTypes.func.isRequired,
  auth: PropTypes.object.isRequired,
};

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

export default connect(mapStateToProps, { logout })(Navbar);
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0