タスク管理ツールをReact + ASP.Coreで作る021認証状態に従って画面表示を制御

[前回](https://qiita.com/mosuma/items/cf50dc4136fe06233412)の続きです。

今回は、認証状態に従って、ログイン画面・タスク管理画面などの画面表示を制御する様にしてみます。


### ガイド
全体Index：[タスク管理ツールをReact + ASP.Coreで作る - Index](https://qiita.com/mosuma/items/88e40dc44e7bff1a54d8)


## 認証機能追加の一連の流れ
- [016サーバーサイドに認証機能関連のクラス追加](https://qiita.com/mosuma/items/728f501bcc7e54352fb8)
- [017クライアントにユーザー登録・ログイン画面と処理を追加](https://qiita.com/mosuma/items/e606cd33423edfdb4118)
- [018認証トークン関連機能をサーバー・クライアント双方に追加](https://qiita.com/mosuma/items/035967eb98259bbd4934)
- [019認証有無に従い制御を実施する機能を追加](https://qiita.com/mosuma/items/82ae859577ab676c06bc)
- [020認証機能をリファクタリング](https://qiita.com/mosuma/items/cf50dc4136fe06233412)
- 021認証状態に従い画面表示を切り替え(本記事)

## サーバーサイドに現在の認証情報


基本的なコンセプトは以下です
・認証されていなければ、ログイン画面を出す。認証されている時だけ、タスク管理画面を出す。



もう少し具体的な仕様に落とすと、以下になります。
- 画面の表示制御
    - 「認証済」ならタスク管理画面を出す
    - 「認証未」ならログイン画面と登録画面を出す。
- 認証状態の把握
    - 「userInfo」Stateのユーザー名(username)に値が入っているかどうかでログイン状態かどうかを判断する。
- 「userInfo」Stateの設定方法
    - アプリケーション読み込み時に、ブラウザにトークンが保存されている場合は、トークンを使ってサーバーサイドに問い合わせを行う。(問い合わせの結果成功の場合は、同時に帰ってくるユーザー名をStateに設定する)
    - ログイン処理を行って成功した場合も、ログイン結果の回答と同時に帰ってくるユーザー名をStateに設定する。この時トークンを合わせてブラウザに保存する
    - トークンを使ってサーバーサイドに問い合わせても失敗した場合(有効期限切れ等)はStateを空にし、「認証未」の状態とみなせるようにする


## サーバーサイドの変更
有効な認証トークンを渡すとユーザー情報を返す関数を追加します

```diff_c_sharp:AccountController.cs
+        [Authorize]
+        [HttpGet]
+        public async Task<ActionResult<UserModel>> GetCurrentUser()
+        {
+            var user = await +_userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email));
+            return CreateUserObject(user);
+        }
```


## クライアントサイドの変更


APIから現状のユーザー情報を取得する通信機能を追加します
```diff_typescript:xxx.ts

const Account = {
+   current: () => axios.get<UserInfo>(`/account`).then((response: AxiosResponse<UserInfo>)=>response.data),
    login: (user: UserFormValues) => axios.post<UserInfo>(`/account/login`, user).then((response: AxiosResponse<UserInfo>)=>response.data),
    register: (user: UserFormValues) => axios.post<UserInfo>(`/account/register`, user).then((response: AxiosResponse<UserInfo>)=>response.data),
}
```

APIから認証状態を把握する機能を追加します。
具体的にはブラウザに保管されているのトークンを取得してユーザー情報取得APIをたたき、返ってきた結果をもとに出し分けを実行します。

```diff_tsx:App.tsx
+import { useEffect, useState } from 'react';
import './App.css';
+import api from './app/api/api';
+import { UserInfo } from './app/models/Account';
import Login from './components/Login';
import Register  from './components/Register';
import { TaskOperationMain } from './components/TaskOperationMain';

function App() {

  
+  const [userInfo, setUserInfo] = useState<UserInfo>({username: '',email: '',token: ''});
+  
+  useEffect(() => {
+    const token = window.localStorage.getItem('tasket_jwt_token');
+    try{
+      api.Account.current().then(user => {
+        window.localStorage.setItem('tasket_jwt_token', user.token);
+        setUserInfo(user);
+      });
+      
+    } catch (error) {
+      console.log(error);
+    }
+  }, []);

  
  return (
    <div>
+      {userInfo.username==='' ? 
+      <>
+        <Login setUserInfo={setUserInfo}/>
+        <Register /> 
+      </>
+      :
+      <TaskOperationMain />
+      }
      
     </div>
  );
}
export default App;
```


### ログイン機能の変更

認証が成功した時に、取得したトークンをブラウザに保存する機能を追加します


```diff_tsx:tabc
import { ErrorMessage,  Formik } from 'formik';
import React from 'react';
import { Form } from 'react-bootstrap';
import * as Yup from 'yup';
import api from '../app/api/api';
import TextInputGeneral from '../app/common/TextInputGeneral';
+ import { UserInfo } from '../app/models/Account';



+ interface Props {
+     setUserInfo: React.Dispatch<React.SetStateAction<UserInfo>>;
+ }


const Login = (
+     {setUserInfo}: Props
    ) => 
{
    
    return (
        <>

        <Formik
            initialValues={{email:'', password: '', error: null}}
            onSubmit={async (values, {setErrors}) => {
                const content = await api.Account.login(values).catch(error => 
                    setErrors({error:'Invalid email or password'}));
                    content?.token && window.localStorage.setItem('tasket_jwt_token', content.token);
+                    if(content){
+                        window.localStorage.setItem('tasket_jwt_token', content.token);
+                        setUserInfo(content);
+                    }
                }
            }
            validationSchema={Yup.object({
                email: Yup.string().required().email(),
                password: Yup.string().required(),
            })}
            >
                {({handleSubmit, isSubmitting, errors, isValid, dirty}) =>(
                    <Form className="ui form" onSubmit={handleSubmit} autoComplete='off'>
                        <h3>Login</h3>
                        <TextInputGeneral name='email' placeholder="Email" />
                        <TextInputGeneral name='password' placeholder="Password" type="password" />
                        <ErrorMessage 
                            name='error' render={() => 
                                <Form.Label style = {{marginBottom:10}} basic color='red' >{errors.error}</Form.Label>
                        }
                        />
                        <button disabled={!isValid || !dirty || isSubmitting} type = 'submit' className="btn btn-primary">Login</button>
                    </Form>
                )}
            </Formik>
        </>
    );

}

export default Login;
```



## 結果

これまでの反映後再実行すると、以下の様にログイン前の状態ではログイン画面(といいつつユーザー登録画面も出てきてしまってまだちょっとカッコ悪いですが)、ログイン後の状態では認証画面は出ずにタスク管理画面が表示されるようになりました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/nnIJjxNXlbE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


今回は以上です。
続きは[次回](https://qiita.com/mosuma/private/be9f6b343fd7af4d1f6c)です