Python Lab #2 ― FastAPI with React

# Python Lab #2 ― FastAPI with React

フロントエンドとして FastAPI で React 画面を返却するサーバーを用意し、バックエンドとして FastAPI で Web API を動作させるサーバーを用意し、ログイン画面から Web API を呼び出すというものを開発してみました。
UI 部分には MUI（Material UI）を採用しました。そのうち、サーバーサイドペイジネーションのデータテーブルにもトライしたいです。

## 画面ショット

**[ログイン画面（React、MUI）]**

![Python_lab-fast_api_01-Login.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/73b5c122-9376-7ae7-ec44-2e6190660f6d.jpeg)

**[ログイン後のホーム画面（React、MUI）]**

とりあえず、MySQL の表 “product_list” の検索結果を表示している。そのうち、サーバーサイドペイジネーションのデータテーブルを実装してみたい。

![Python_lab-fast_api_02-Home.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/423c0be7-3de5-ca1f-b3d3-8664529ad821.jpeg)

## 動機

* 過去に Flask、React-Bootstrap、axios の基礎知識を習得したことがある。
* FastAPI は、Flask より性能が良いとされている。なので、FastAPI の基礎知識を習得してみたくなった。JWT（Java Web Token）も含めて。
* MUI（Material UI）は、Google の Material Design に基づいた UI ライブラリとのこと。なので、MUI の基礎知識を習得してみたくなった。
* 最新のブラウザーの JavaScript に Fetch API が標準で実装されている。なので、Fetch API の基礎知識を習得してみたくなった。
* ORM（Object-Relational Mapping）は、MyBatis の基礎知識しか習得したことがない。MyBatis は、「SQL 文とオブジェクトをマッピング」する ORM である。なので、「表とオブジェクトをマッピング」する ORM を知るために SQLAlchemy の基礎知識を習得してみたくなった。

## 体得したこと

**JWT を安全な場所に保存したほうがよい**

Local Storage に保存するのは、悪意のサイトから Local Storage を読み出される可能性があるため、あまり安全ではないとされている。
Cookie に 保存するのが、比較的に安全であるとされている。

以下を考慮する。

* レスポンスヘッダーに “Set-Cookie: ...; Secure; HttpOnly;” を指定する。
* サーバーセッションに実際の情報を保存する。← Local Storage の Cookie のいずれに保存するとしても

なので、今回は、Cookie に保存することにしてみた。実装が楽かな？と思ったので。

**フロントエンドとバックエンドの二つのサーバーを立てて、バックエンド側で Cookie を設定できるようにするためには様々な設定をする**

Cookie での実装は楽だろうと思いきや、かなりハマってしまった（8 時間くらい...）。ただ、Cookie の知識を深めることができてよかった。
以下のように、セキュリティ向けの設定値を変更する必要がある。

* パックエンドで、特定のフロントエンドのサイトのページからアクセスされることを許可する。
→  “app.add_middleware()” に “allow_origins = [...]” を設定する。これにより、悪意のサイトからアクセスされてもアクセス拒否することができる。今回の Web API はあらゆるサイトに公開するものではないので、この設定ができる。
* バックエンドで、フロントエンドのサイトの Cookie に保存することを許可する。
→ レスポンスヘッダーに “Set-Cookie: ...; Secure; HttpOnly; SameSite=none” を指定する。
* フロントエンドで、異なるサイトの資格情報（Cookie など）を送信することを許可する。
→ JavaScript の Fetch API のオプションに “credentials: 'include'” を指定する。

**JWT のプリミティブそのものでは Java（Tomcat）のようなセッションの仕組みを提供しているわけではない**

セションの情報をデータベース等に保持する作り込みが必要とのこと。
JWT には単に規則的な文字列を振っているだけなので、ログアウトで Cookie 中の JWT を消す作りにした（レスポンスヘッダーに “Set-Cookie: access_token=;” で access_token を消すようにした）としても、次のリクエストのリクエストヘッダー “Cookie: accss_token=” で前回にログインしたときの access_token の文字列を指定しなおせば、ログイン状態を継続した状態にできてしまう。また、タイムアウトが発生すれば（ログインしてから ACCESS_TOKEN_EXPIRE_MINUTES の時間を超えれば）、セションが無効とみなされる。

## 今後、研究したいこと

* セッションの仕組みを実装する。セッションの情報をデータベースに保持したり破棄したり。
* MUI のサーバーサイドペイジネーションのデータテーブルを実装する。
* 見た目の向上。CSS の仕組みを研究するところから。

## このページの読者の前提

初級を抜け出した人向けです。

* Web や HTTP に関する基礎知識を持っていること。
* 自分で MySQL、Node.js、Python、React の環境を構築し、ソースコードを取り込んで、プログラムの実行ができること。
* 自分でざっと Python、React のソースコードを読んで理解できること。

## フォルダー・ファイル構成

* フォルダー・ファイル
    ```フォルダー・ファイル
    D:\Development\PyCharmProjects\lab-fast_api
    │  LICENSE
    │  README.md
    │  requirements.txt
    ├─ backend
    │  │  backend_main.py → 後述
    │  ├─ action
    │  │    authenticate_user_action.py → 後述
    │  │    manipulate_products_action.py
    │  ├─ database
    │  │    connect_database.py → 後述
    │  │    create_database.py → 後述
    │  ├─ mapper
    │  │    products_mapper.py
    │  │    users_mapper.py → 後述
    │  ├─ response
    │  │    list_response.py
    │  │    status_response.py
    │  └─ templates
    │     │  index.html
    │     └─ static
    └─ frontend
       │  frontend_main.py → 後述
       │  package-lock.json
       │  package.json
       │  README.md
       │  tsconfig.json
       ├─ build
       │  │  ・・・
       │  └─ static
       │      ・・・
       ├─ node_modules
       │    ・・・
       ├─ public
       │    favicon.ico
       │    index.html
       │    logo192.png
       │    logo512.png
       │    manifest.json
       │    robots.txt
       └─ src
          │  index.css
          │  index.tsx → 後述
          └─ pages
             │  Home.tsx → 後述
             │  Login.tsx → 後述
             │  Settings.tsx
             │  Top.tsx
             └─ components
                  Footer.tsx
                  Header.tsx
                  Logout.tsx
    ```

## 環境の構築からプログラムの実行まで

**事前**

1. Python をインストールする。
2. MySQL をインストールする。 
3. Node.js をインストール、セットアップする。
4. Pycharm などで “D:\Developments\PyCharmProjects” 配下に Python プロジェクト “lab-fast_api” を作成する。
5. Pycharm などで requirements.txt に記載された Python パッケージをインストールする。

**バックエンド**

1. “D:\Developments\PyCharmProjects\lab-fast_api\backend” 配下の各種フォルダーに各種ソースコードを置く。
2. データベースを作成する。
    ```bat:コマンド・プロンプト
    C:\Users\xxxxx> D:
    D:\> cd D:\Developments\PyCharmProjects\lab-fast_api\backend
    D:\Developments\PyCharmProjects\lab-fast_api\backend> python.exe .\database\create_database.py
    D:\Developments\PyCharmProjects\lab-fast_api\backend> exit
    ```
6. Pycharm などで backend_main.py を実行する。

**フロントエンド**

1. React プロジェクトを作成する。
    ```bat:コマンド・プロンプト
    C:\Users\xxxxx> D:
    D:\> cd D:\Development\PyCharmProjects\lab-fast_api
    D:\Developments\PyCharmProjects\lab-fast_api> npx create-react-app frontend --template typescript
    D:\Development\PyCharmProjects\lab-fast_api> cd frontend
    D:\Development\PyCharmProjects\lab-fast_api\frontend> npm install react-router-dom
    D:\Development\PyCharmProjects\lab-fast_api\frontend> npm install @mui/material @emotion/react @emotion/styled
    D:\Development\PyCharmProjects\lab-fast_api\frontend> npm run build
    D:\Development\PyCharmProjects\lab-fast_api\frontend> exit
    ```
2. “D:\Development\PyCharmProjects\lab-fast_api\frontend” 配下の各種フォルダーに各種ソースコードを置く。
3. Pycharm などで frontend_main.py を実行する。
4. ブラウザーで “http://localhost:3000” をアクセスする。

## ソースコードの簡単な説明

**バックエンド**

* SQLAlchemy データベース定義
    ```python:database\create_database.py
    ・・・
    
    # Import Libraries
    import sys
    from sqlalchemy import create_engine, text
    from sqlalchemy_utils import create_database, drop_database
    from sqlalchemy.orm import sessionmaker, scoped_session
    from backend.mapper.products_mapper import product_list_base
    from backend.mapper.users_mapper import user_list_base
    
    # Display
    print('lab-FastAPI - DDL/DML')
    print('root password is "********"')
    password = input('Enter password: ')
    
    # Constants
    DIALECT = 'mysql'
    DRIVER = 'mysqlconnector'
    USER = 'root'
    HOST = 'localhost'
    PORT = '3306'
    DATABASE = 'lab_fast'
    DATABASE = f'{DIALECT}+{DRIVER}://{USER}:{password}@{HOST}:{PORT}/{DATABASE}?charset=utf-8'
    
    CMD_DDLS = [
        text('DROP ROLE IF EXISTS lab_fast_role'),
        text('CREATE ROLE lab_fast_role'),
        text('DROP USER IF EXISTS \'lab_fast_user\'@\'localhost\''),
        text('CREATE USER \'lab_fast_user\'@\'localhost\' IDENTIFIED BY \'Asdf1234\' DEFAULT ROLE lab_fast_role'),
        text('GRANT SELECT, INSERT, UPDATE, DELETE ON lab_fast.product_list TO lab_fast_role'),
        text('GRANT SELECT, INSERT, UPDATE, DELETE ON lab_fast.user_list TO lab_fast_role'),
    ]
    CMD_DMLS = [
        text('''
            INSERT INTO LAB_FAST.PRODUCT_LIST ( NAME, REMARK ) VALUES
                ( 'Apple', 'Made in Japan.' ),
                ( 'Orange', 'Made in America.' )
        '''),
        text('''
            INSERT INTO LAB_FAST.USER_LIST ( NAME, PASSWORD, MAIL, REMARK ) VALUES
                ( 'root', HEX(AES_ENCRYPT('Asdf1234', 'Asdf1234Asdf1234')), 'root@xxxx.com', 'Administrator' ),
                ( 'power-user', HEX(AES_ENCRYPT('Asdf1234', 'Asdf1234Asdf1234')), 'power.user@xxxx.com', 'Power User' )
        '''),
    ]
    
    
    # Main
    def main() -> None:
    
        # Engine
        engine = create_engine(DATABASE, echo=True)
        session = scoped_session(sessionmaker(autocommit=True, autoflush=True, bind=engine))
    
        # Drop Database
        try:
            drop_database(engine.url)
        except Exception:    # noqa
            pass
    
        # Create Database
        create_database(engine.url)
        conn = engine.connect()
        product_list_base.metadata.create_all(bind=engine)
        product_list_base.query = session.query_property()
        user_list_base.metadata.create_all(bind=engine)
        user_list_base.query = session.query_property()
    
        # Grant
        for cmd in CMD_DDLS:
            _ = conn.execute(cmd)
            conn.commit()
    
        # Insert Test Data
        for cmd in CMD_DMLS:
            _ = conn.execute(cmd)
            conn.commit()
    
        sys.exit(0)
    
    
    # Goto Main
    if __name__ == '__main__':
        main()
    ```
    **説明**
    SQLAlchemy の表削除関数 “drop_database()”、表作成関数 “create_database()” を呼び出して、表を作成する。
    また、SQLAlchemy の SQL 文直接実行関数 “execute()” を呼び出して、GRANT と INSERT を行う。
* FastAPI Web API バックエンドサーバー
    ```python:backend_main.py
    ・・・
    
    # When wrote this source code, referred this page 'https://fastapi.tiangolo.com/ja/tutorial/security/oauth2-jwt/'
    
    # Import Libraries
    
    # import os
    # import sys
    import uvicorn
    
    import secrets
    from jose import JWTError, jwt
    from typing import Optional
    from pydantic import BaseModel
    from datetime import datetime, timedelta
    
    from fastapi import FastAPI, Depends, HTTPException, Request, Response, status
    from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    from fastapi.staticfiles import StaticFiles
    from fastapi.middleware.cors import CORSMiddleware
    
    from action.authenticate_user_action import get_user_and_password, get_user
    from action.manipulate_products_action import read_products
    from backend.response.status_response import JsonStatusResponse
    
    # Constants
    SECRET_KEY = secrets.token_hex(32)  # example: 'c13b4b4808d0eed9323e44897315b4fea0c023313585afd9c0d6896e769490fe'
    ALGORITHM = 'HS256'
    ACCESS_TOKEN_EXPIRE_MINUTES = 30
    ORIGINS = [
        'http://localhost:3000',
    ]
    
    
    # Token
    class Token(BaseModel):
        access_token: str
        token_type: str
    
    
    # TokenData
    class TokenData(BaseModel):
        username: Optional[str] = None
    
    
    # User
    class User(BaseModel):
        username: str
    
    
    # FastAPI Settings
    app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
    app.mount(path='/static', app=StaticFiles(directory='.\\templates\\static'), name='static')
    app.add_middleware(
        CORSMiddleware,    # noqa
        allow_origins=ORIGINS,
        allow_credentials=True,
        allow_methods=['*'],
        allow_headers=['*'],
    )
    templates = Jinja2Templates(directory='.\\templates')
    
    # OAuth2
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')
    
    
    # Create JWT at /token Endpoint Request
    def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
        to_encode = data.copy()
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(minutes=15)
        to_encode.update({'exp': expire})
        encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
        return encoded_jwt
    
    
    # Check JWT Is Valid or Not at Some Endpoint Request
    async def get_current_user(request: Request):
        credentials_exception = HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Could not validate credentials',
            headers={'WWW-Authenticate': 'Bearer'},
        )
        token = ''
        # read from request header of 'Cookie: access_token={access_token}'
        cookie = request.cookies.get('access_token')
        if cookie is not None and cookie != '':
            token = cookie
        # read from request header of 'Authorization: "Bearer {access_token}"'
        authorization = request.headers.get('Authorization')
        if authorization is not None and authorization != '':
            scheme, _, token = authorization.partition(' ')
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            username: str = payload.get('username')
            if username is None:
                raise credentials_exception
            token_data = TokenData(username=username)
        except JWTError:
            raise credentials_exception
        user = get_user(username=token_data.username)
        if user is None:
            raise credentials_exception
        return user
    
    
    # / (Return index.html)
    @app.get("/", response_class=HTMLResponse)
    async def root(request: Request):
        return templates.TemplateResponse('index.html', {'request': request, 'message': 'hello world'})
    
    
    # /auth (Return the JWT on Response Header/Body)
    @app.post('/auth', response_model=Token)
    async def login_for_access_token(
            response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
        user = get_user_and_password(form_data.username, form_data.password)
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail='Incorrect username or password',
                headers={'WWW-Authenticate': 'Bearer'}
            )
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={'username': user.username}, expires_delta=access_token_expires
        )
        # write to response header of 'Set-Cookie: access_token={access_token}'
        response.set_cookie(key='access_token', value=f'{access_token}', httponly=True, secure=True, samesite='none')
        # return to value of JSON
        return {'access_token': access_token, 'token_type': 'bearer'}
    
    
    # /unauth (No Error Process)
    @app.post('/unauth')
    async def unauth(response: Response):
        # write to response header of 'Set-Cookie: access_token=""'
        response.delete_cookie(key="access_token", httponly=True, secure=True, samesite='none')
        return JsonStatusResponse()
    
    
    # /is_valid (Need the JWT on Request Header)
    @app.post('/is_valid')
    async def is_valid(current_user: User = Depends(get_current_user)):    # noqa
        return JsonStatusResponse()
    
    
    # /products (Need the JWT on Request Header)
    @app.get('/products')
    async def get_products(current_user: User = Depends(get_current_user)):    # noqa
        return read_products()
    
    
    # Web API Backend Web Server
    if __name__ == '__main__':
    #    sys.stdout = open(os.devnull, 'w')    # Discard stdout
    #    sys.stderr = open(os.devnull, 'w')    # Discard stderr
        uvicorn.run(app, host='0.0.0.0', port=3001, server_header=False)
    ```
    ログイン画面向けのエンドポイント “http://localhost:3001/auth” において、ユーザーとパスワードが正しいか否かを判定する。
    参考にした FastAPI ホームページのソースコード “<https://fastapi.tiangolo.com/ja/tutorial/security/oauth2-jwt/>” では、レスポンスボディに JWT の JSON を返却するものの、その後に JWT をどこに保存するかまでは言及していない。今回、レスポンスヘッダーに “Set-Cookie: access_token={jwt}” を返却してブラウザーの Cookie にも保存するようにしてみた。
    ホーム画面向けのエンドポイント “http://localhost:3001/products” において、リクエストヘッダーの “Cookie: access_token={jwt}” または “Authorization: Bearer {jwt}” から JWT を受け取って JWT の文字列を検証する。

* Web API - 認証
    ```python:action\authenticate_user_action.py
    ・・・
    
    # Import Libraries
    import binascii
    from Crypto.Cipher import AES
    from backend.mapper import users_mapper as model
    from backend.database import connect_database as connect
    
    # Crypto Key
    CRYPTO_KEY = 'Asdf1234Asdf1234'
    
    
    # Strip Padding String From ECB Decrypto String
    def unpad(s):
        return s[:-ord(s[len(s) - 1:])]
    
    
    # Decrypto
    def decrypto(password_aes) -> str:
        password_bin = binascii.unhexlify(password_aes)
        decipher = AES.new(CRYPTO_KEY.encode('utf-8'), AES.MODE_ECB)
        dec = decipher.decrypt(password_bin)
        return unpad(dec).decode('utf-8')
    
    
    # Get User and Verify Password
    def get_user_and_password(username: str, password: str) -> []:
        user_list = connect.session.query(
            model.UserList.name.label("username"),
            model.UserList.password).filter(model.UserList.name == username).first()
        password_aes = user_list[1]    # column name "password"
        password_dec = decrypto(password_aes)
        if password != password_dec:
            return False
        return user_list
    
    
    # Get User
    def get_user(username: str) -> []:
        user_list = connect.session.query(
            model.UserList.name.label("username")).filter(model.UserList.name == username).first()
        return user_list
    ```
* Web API - データベース
    ```python:database\connect_database.py
    ・・・
    
    # Import Libraries
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker, scoped_session
    
    # Constants
    DIALECT = 'mysql'
    DRIVER = 'mysqlconnector'
    USER = 'lab_fast_user'
    PASSWORD = 'Asdf1234'
    HOST = 'localhost'
    PORT = '3306'
    DB = 'lab_fast'
    DATABASE = f'{DIALECT}+{DRIVER}://{USER}:{PASSWORD}@{HOST}:{PORT}/{DB}?charset=utf-8'
    
    # Engine
    engine = create_engine(DATABASE, echo=True)    # True: SQL statement log
    session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    session = scoped_session(session_local)
    ```
* Web API - マッパー
    ```python:mapper\users_mapper.py
    ・・・
    
    # Import Libraries
    from sqlalchemy import Column, Integer, String, DateTime
    from sqlalchemy.sql.functions import current_timestamp
    from sqlalchemy.ext.declarative import declarative_base
    
    # Base
    user_list_base = declarative_base()
    
    
    # USER_LIST
    class UserList(user_list_base):
        __tablename__ = 'user_list'
        id = Column(Integer, primary_key=True, autoincrement=True)
        name = Column(String(32), nullable=False, unique=True)
        password = Column(String(416), nullable=False)
        mail = Column(String(64), nullable=False, unique=True)
        remark = Column(String(768), nullable=False)
        created_at = Column(DateTime, server_default=current_timestamp())
        updated_at = Column(DateTime, server_default=current_timestamp())
    ```

**フロントエンド**

* FastAPI React フロントエンドサーバー
    ```python:frontend_main.py
    ・・・
    
    # Import Libraries
    
    # import os
    # import sys
    import uvicorn
    
    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse
    from fastapi.exceptions import HTTPException
    from fastapi.templating import Jinja2Templates
    from fastapi.staticfiles import StaticFiles
    
    
    # FastAPI Settings
    app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
    app.mount(path='/static', app=StaticFiles(directory='.\\build\\static'), name='static')
    templates = Jinja2Templates(directory='.\\build')
    
    
    # / (Return index.html)
    @app.get("/", response_class=HTMLResponse)
    async def root(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    
    
    # When Manipulate [Rewind] [Forward] [Refresh] Button [URL] Textbox at Browser
    @app.exception_handler(404)
    async def not_found(request: Request, ex: HTTPException):    # noqa
        return templates.TemplateResponse('index.html', {'request': request})
    
    
    # React Frontend Web Server
    if __name__ == '__main__':
    #    sys.stdout = open(os.devnull, 'w')    # Discard stdout
    #    sys.stderr = open(os.devnull, 'w')    # Discard stderr
        uvicorn.run(app, host='0.0.0.0', port=3000, server_header=False)
    ```
* React index.html 画面
    ```typescript:src\index.tsx
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    
    import Top from './pages/Top';
    import Login from './pages/Login';
    import Home from './pages/Home';
    import Settings from './pages/Settings';
    
    import {BrowserRouter, Route, Routes} from 'react-router-dom';
    
    const root = ReactDOM.createRoot(
      document.getElementById('root') as HTMLElement
    );
    root.render(
      <React.StrictMode>
        <BrowserRouter>
          <Routes>
            <Route path='/' element={<Top />} />
            <Route path='/login' element={<Login />} />
            <Route path='/home' element={<Home />} />
            <Route path='/settings' element={<Settings />} />
            <Route path='*' element={<h3>Not Found.</h3>} />
          </Routes>
        </BrowserRouter>
      </React.StrictMode>
    );
    ```
    ブラウザーで “http://localhost:3000” をアクセスしたとき、index.html を表示する。
    また、ブラウザーで [戻る] ボタン、[進む] ボタン、[更新] ボタンを押下したり [URL] テキストボックスに “http://localhost:3000/login” 等を指定したりして 404（Not Found）になったときに index.html を表示することで、React ルーターに相応の処理を行わせる。
* React ログイン画面
    ```typescript:src\pages\Login.tsx
    import {Grid, Box, Button, TextField} from '@mui/material';
    import CssBaseline from '@mui/material/CssBaseline';
    
    import {useNavigate} from "react-router-dom";
    import {useState, useEffect, useRef} from 'react';
    
    import Header from './components/Header';
    import Footer from './components/Footer';
    import Logout from './components/Logout';
    
    const styles = {
      marginLeft: 1, marginRight: 0,
    };
    
    function Login() {
      const [message, setMessage] = useState('');
      const [userName, setUserName] = useState('');
      const [password, setPassword] = useState('');
      const navigate = useNavigate();
      const loaded = useRef(false);
      useEffect(() => {
        if (loaded.current) {
          return;
        }
        loaded.current = true;
        Logout();
      }, []);
      const login = async () => {
        if (! userName) {
          setMessage('User Name is required.');
          return;
        }
        if (! password) {
          setMessage('Password is required.');
          return;
        }
        const headers = new Headers();
        const formData = new FormData();
        formData.append('username', userName);
        formData.append('password', password);
        const request = new Request('http://localhost:3001/auth', {
          method: 'POST',
          headers: headers,
          body: formData,
          credentials: 'include',
        });
        let error = false;
        await fetch(request)
        .then (response => {
          if (! response.ok) {
            error = true;
          }
          return response.json();
        })
        .then (data => {
          if (error) {
            setMessage('User Name or Password is invalid.');
            return;
          }
          const token = data.access_token;
          navigate('/home');
        })
        .catch (error => {
          setMessage('Network trouble has occurred.');
          return;
        });
      }
      return (
        <>
          <CssBaseline />
          <Header logoutButton={false} />
          <Box
            component="form" noValidate autoComplete="off"
            display='flex'
            flexDirection='column'
            justifyContent='space-between'
            sx={{'& .MuiTextField-root': {m: 1, width: '30ch'}}}
          >
            <Grid sx={styles}>
              <h3>Login</h3>
            </Grid>
            <Grid sx={styles}>
              {message}
            </Grid>
            <Grid item xs={16}>
              <TextField id="userName" label="User Name" variant="outlined" onChange={(e) => setUserName(e.target.value)} />
            </Grid>
            <Grid item xs={16}>
              <TextField id="password" label="Password" type="password" autoComplete="" onChange={(e) => setPassword(e.target.value)} />
            </Grid>
            <Grid sx={styles}>
              <Button id="login" variant="contained" color="primary" onClick={login} sx={{textTransform: "none"}}>Login</Button>
            </Grid>
          </Box>
          <Footer />
        </>
      );
    }
    
    export default Login;
    ```
    エンドポイント “http://localhost:3001/auth” を呼び出す。
* React ホーム画面
    ```typescript:src\pages\Home.tsx
    import {Grid, Box, Tabs, Tab} from '@mui/material';
    import CssBaseline from '@mui/material/CssBaseline';
    
    import {useNavigate} from "react-router-dom";
    import {useState, useEffect, useRef} from 'react';
    
    import Header from './components/Header';
    import Footer from './components/Footer';
    
    const styles = {
      marginLeft: 1, marginRight: 0,
    };
    
    function Home() {
      const [message, setMessage] = useState('');
      const emptyData: {name: string, remark: string}[] = [];
      const [data, setData] = useState(emptyData);
      const navigate = useNavigate();
      const getProducts: () => void = async () => {
        const headers = new Headers();
        const request = new Request('http://localhost:3001/products', {
          method: 'GET',
          headers: headers,
          credentials: 'include',
        });
        let error = false
        await fetch(request)
        .then (response => {
          if (! response.ok) {
            error = true;
            return {
              response: {
                status: 'action-ng',
                message: 'Session timeout has occurred.',
              },
            };
          }
          return response.json();
        })
        .then (data => {
          if (error) {
            setMessage(data.response.message);
            return;
          }
          setData(data.response.list);
        })
        .catch (error => {
          setMessage('Network trouble has occurred.');
          return;
        });
      }
      const loaded = useRef(false);
      useEffect(() => {
        if (loaded.current) {
          return;
        }
        loaded.current = true;
        getProducts();
      }, []);
      function gotoHome() {
        navigate('/home');
        return;
      };
      function gotoSettings() {
        navigate('/settings');
        return;
      };
      return (
        <>
          <CssBaseline />
          <Header />
          <Box
            component="form" noValidate autoComplete="off"
            display='flex'
            flexDirection='column'
            justifyContent='space-between'
            sx={{'& .MuiTextField-root': {m: 1, width: '30ch'}}}
          >
            <Grid sx={styles}>
              <Tabs value="Home">
                <Tab id="home" label="Home" value="Home" style={{fontSize: 18, fontWeight: 'bold'}} sx={{textTransform: "none"}} onClick={gotoHome} />
                <Tab id="settings" label="Settings" value="Settings" sx={{textTransform: "none"}} onClick={gotoSettings} />
              </Tabs>
            </Grid>
            <Grid sx={styles}>
              {message}
            </Grid>
            {message === '' && (
              <Grid sx={styles}>
                <br />
                {data.map(row => (
                  <>
                    {row.name}, {row.remark}<br />
                  </>
                ))}
              </Grid>
            )}
          </Box>
          <Footer />
        </>
      );
    }
    
    export default Home;
    ```
    エンドポイント “http://localhost:3001/products” を呼び出し、検索結果を表示する。

## 全ソースコードの置き場所

https://github.com/tengbu-haoshou/Python_lab-fast_api

## 参考

**書籍**

https://www.amazon.co.jp/exec/obidos/ASIN/4815619484

**React、FastAPI**

https://zenn.dev/sawao/articles/15a9cf0e3360a7

**FastAPI で JWT**

https://fastapi.tiangolo.com/ja/tutorial/security/oauth2-jwt/


https://note.com/mega_gorilla/n/ncec503b5eb8d

**Fetch API と Axios**

https://qiita.com/manzoku_bukuro/items/3e5bb0a678ebe7a2d2c2

https://qiita.com/iwakeniwaken/items/01f78eca5258321bc305

**Fetch API の credentials: 'include' 指定での資格情報の送信**

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials