[MERN⑦] React User Authentication

~~~~~~~~~~ (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`なので、`authLinks`と`guestLinks`が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);
```