Python Lab #1 ― Flask with React/Vue

# Python Lab #1 ― Flask with React/Vue

Flask で Web API が動作する Web サーバーを用意し、Vue と React のそれぞれのログイン画面から Web API を呼び出すというものを開発してみました。
今にして思うに、FastAPI を採用したほうがよかったかもしれません。

## 画面ショット

今にして思うに、React-Bootstrap を使うのではなく、MUI（Material UI）を使って Vuetify に近い Look & Feel を実現すればよかったかもしれません。ただ、MUI（Material UI）を使いこなすのは React-Bootstrap や Vuetify より難しそうな感じがします。

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

![Python_lab-flask_01-Login-React.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/b57f67a4-8c74-a5d7-24ea-3e98bfbca52a.jpeg)

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

![Python_lab-flask_03-Home-React.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/5537812f-be4e-50f2-4e16-ac5e1fb8a343.jpeg)

**[ログイン画面（Vue、Vuetify）]**

![Python_lab-flask_02-Login-Vue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/5a8d65bf-56ff-9abb-a0a5-a2558ec3591d.jpeg)

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

![Python_lab-flask_04-Home-Vue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529613/adbbf447-3bfd-aa7a-8e00-13ece2d659e2.jpeg)

## 動機

* 過去に “Web 開発再入門” にて、ある程度の Vue、Vuetify、Java、SpringBoot の基礎知識を習得したことがある。
* 今後のプロジェクトで React を使用することが多くなりそう。なので、今回、React、React 関連（React-Router-Dom、React-Redux、React-Bootstrap など）および TypeScript の基礎知識を習得をしようと考えた。
* Flask を使えば簡単に Web API が準備できそうなので、ついでに Flask および Flask 関連（FlaskLogin、FlaskForm、jsonify など）も勉強することにした。
* Vue と React それぞれでログイン画面を開発してみることにした。

## 体得できたこと

**ささっと Web API を作るときには Flask は本当に便利**

* 例えば、クライアント・サイド・デモを作るときに有用である。
* もし、しっかりした Web API を作るならば、Java SpringBoot のようにバリデーターのようなものや DAO のようなものなどをしっかり設計する必要がある。

**Vue と React の違いについて感じたこと（ただの感想）**

* 確かに Vue は、直観的な感じがする（他の動的 HTML テンプレートエンジン、例えば JSP、PHP、Thymeleaf とかを知っている人なら分かりやすい気がする）。
* React は、モダンな Javascript のさまざまな文法を駆使する必要があるらしく、ちょっと戸惑う気がする。ただ、画面パーツのモジュール化とかやり始めたら React のほうがやりやすいような気がする（大小さまざまなモジュール化が簡単にできそうな気がする）。

**HTML 画面パーツ間のデフォルトのマージンについて（不勉強なため、よく分からない...）**

* Vuetify は、マージンが適度に広くとられている。
* React-Bootstrap は、マージンがないようである（なので、複数の画面パーツがくっついてしまうみたいである）。CSS を学習ののち、マージン調整を実装する必要がある。

**Java（Tomcat）と Flask で、セションの考え方や実装が大きく異なる**

* Java（Tomcat）においてセッションの情報をメモリに保存する。アプリケーション側のログアウトでセッションを破棄すると、メモリの情報が破棄される。また、アイドルタイムアウトが発生すれば、Java 側でセッションを破棄されることになる。

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

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

* セッションの仕組みを実装する。セッションの情報をデータベースに保持したり破棄したり。
* 見た目の向上。CSS の仕組みを研究するところから。

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

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

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

## フォルダー・ファイル構成
* フォルダー・ファイル
    ```:フォルダー・ファイル
    D:\Development\PyCharmProjects\lab-flask
    │     LICENSE
    │     main.py → 後述
    │     README.md
    │     requirements.txt
    │     settings.py → 後述
    ├─ action
    │     auth_action.py → 後述
    │     info_action.py
    │     unauth_action.py
    ├─ mapper
    │     select_mapper.py → 後述
    ├─ resources-react ← ビルドして作成した React リソース
    │  │     asset-manifest.json
    │  │     favicon.ico
    │  │     index.html
    │  │     manifest.json
    │  │     robots.txt
    │  └─ static
    │        ・・・
    ├─ resources-vue ← ビルドして作成した React リソース
    │  │     favicon.ico
    │  │     index.html
    │  └─ assets
    │        ・・・
    ├─ sql
    │     lab-flask-ddl.bat
    │     lab-flask-ddl.sql → 後述
    │     lab-flask-dml.bat
    │     lab-flask-dml.sql → 後述
    ├─ validator
    │     auth_validator.py → 後述
    ├─ web-react ← React プロジェクト
    │  │     package-lock.json
    │  │     package.json
    │  │     README.md
    │  │     tsconfig.json
    │  ├─ build
    │  │  │     asset-manifest.json
    │  │  │     favicon.ico
    │  │  │     index.html
    │  │  │     manifest.json
    │  │  │     robots.txt
    │  │  └─ static
    │  │        ・・・
    │  ├─ node_modules
    │  │     ・・・  
    │  ├─ public
    │  │     favicon.ico
    │  │     index.html
    │  │     manifest.json
    │  │     robots.txt
    │  └─ src
    │     │     index.css
    │     │     index.tsx
    │     │     logo.svg
    │     │     react-app-env.d.ts
    │     │     reportWebVitals.ts
    │     ├─ store
    │     │     reducer.js
    │     └─ views
    │           FlaskHome.tsx
    │           FlaskLogin.tsx → 後述
    │           FlaskSettings.tsx
    │           FlaskTop.tsx
    └─ web-vue ← Vue プロジェクト
       │     .eslintrc.cjs
       │     index.html
       │     package-lock.json
       │     package.json
       │     README.md
       │     vite.config.js
       ├─ node_modules
       │     ・・・
       ├─ public
       │     favicon.ico
       └─ src
          │     App.vue
          │     main.js
          ├─ assets
          │     base.css
          │     logo.svg
          │     main.css
          ├─ router
          │     index.js
          ├─ stores
          │     store.js
          └─ views
                FlaskHome.vue → 後述
                FlaskLogin.vue
                FlaskSettings.vue
                FlaskTop.vue
    ```

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

1. Python をインストールする。
2. MySQL をインストールする。
3. Node.js をインストール、セットアップする。
4. Pycharm などで “D:\Development\PyCharmProjects” 配下に Python プロジェクト  “lab-flask” を作成する。
5. “D:\Development\PyCharmProjects\lab-flask” 配下に、Vue プロジェクト “web-vue” を作成する。
    ```bat:コマンド・プロンブト
    C:\Users\xxxx> D:
    D:> cd D:\Development\PyCharmProjects\lab-flask
    D:\Development\PyCharmProjects\lab-flask> npm create vue

    Vue.js - The Progressive JavaScript Framework

    √ Project name: ... web-vue
    √ Add TypeScript? ... No / Yes  ← No
    √ Add JSX Support? ... No / Yes  ← No
    √ Add Vue Router for Single Page Application development? ... No / Yes  ← Yes
    √ Add Pinia for state management? ... No / Yes  ← Yes
    √ Add Vitest for Unit Testing? ... No / Yes  ← No
    √ Add an End-to-End Testing Solution? » No  ← No
    √ Add ESLint for code quality? ... No / Yes  ← No
    √ Add Prettier for code formatting? ... No / Yes ← No
    ・・・
    D:\Development\PyCharmProjects\lab-flask> cd web-vue
    D:\Development\PyCharmProjects\lab-flask\web-vue> npm install
    D:\Development\PyCharmProjects\lab-flask\web-vue> npm install @mdi/font
    D:\Development\PyCharmProjects\lab-flask\web-vue> npm install axios
    D:\Development\PyCharmProjects\lab-flask\web-vue> exit
    ```
4. “D:\Developments\PyCharmProjects\lab-flask” 配下に、React プロジェクト “web-react” を作成する。
    ```bat:コマンド・プロンブト
    C:\Users\xxxx> D:
    D:> cd D:\Development\PyCharmProjects\lab-flask
    D:\Developments\PyCharmProjects\lab-flask> npx create-react-app web-react --template typescript
    ・・・
    D:\Development\PyCharmProjects\lab-flask> cd web-react
    D:\Development\PyCharmProjects\lab-flask\web-react> npm install react-router-dom
    D:\Development\PyCharmProjects\lab-flask\web-react> npm install react-bootstrap bootstrap
    D:\Development\PyCharmProjects\lab-flask\web-react> npm install axios
    D:\Development\PyCharmProjects\lab-flask\web-react> npm install redux react-redux
    D:\Development\PyCharmProjects\lab-flask\web-react> @rem npm start ← テスト用
    D:\Development\PyCharmProjects\lab-flask\web-react> npm run build
    D:\Development\PyCharmProjects\lab-flask\web-react> robocopy build ../resources-react /MIR /DCOPY:DAT
    D:\Development\PyCharmProjects\lab-flask\web-react> exit
    ```
6. “D:\Development\PyCharmProjects\lab-flask” 配下の各種フォルダーに各種ソースコードを置く。
8. Vue プロジェクトをビルドする。
    ```bat:コマンド・プロンブト
    C:\Users\xxxx> D:
    D:> cd D:\Development\PyCharmProjects\lab-flask\web-vue
    D:\Development\PyCharmProjects\lab-flask\web-vue> @rem npm run dev ← テスト用コマンド
    D:\Development\PyCharmProjects\lab-flask\web-vue> npm run build
    D:\Development\PyCharmProjects\lab-flask\web-vue> exit
    ```
8. React プロジェクトをビルドする。
    ```bat:コマンド・プロンブト
    C:\Users\xxxx> D:
    D:> cd D:\Development\PyCharmProjects\lab-flask\web-react
    D:\Development\PyCharmProjects\lab-flask\web-react> @rem npm start ← テスト用コマンド
    D:\Development\PyCharmProjects\lab-flask\web-react> npm run build
    D:\Development\PyCharmProjects\lab-flask\web-react> robocopy build ../resources-react /MIR /DCOPY:DAT
    D:\Development\PyCharmProjects\lab-flask\web-react> exit
    ```
9. mysql コマンドでデータベースを作成する。
10. Pycharm などで requirements.txt に記載されたパッケージをインストールする。
11. Pycharm などで main.py を実行する。
12. ブラウザーで “http://localhost:8080” をアクセスする。

## データベースの簡単な説明

MySQL の DDL 文、DML 文です。

* DDL 文
    ```sql:lab-flask-ddl.sql
    ・・・
    
    -- Role
    
    DROP ROLE IF EXISTS LAB_FLASK_ROLE;
    CREATE ROLE LAB_FLASK_ROLE;
    
    -- User
    
    DROP USER IF EXISTS 'LAB_FLASK_USER'@'localhost';
    CREATE USER 'LAB_FLASK_USER'@'localhost' IDENTIFIED BY 'Asdf1234' DEFAULT ROLE LAB_FLASK_ROLE;
    
    -- Schema
    
    DROP SCHEMA IF EXISTS LAB_FLASK;
    CREATE SCHEMA LAB_FLASK;
    
    -- USER_LIST
    
    CREATE TABLE LAB_FLASK.USER_LIST (
    	USER_ID						CHAR(26)		NOT NULL UNIQUE,
    	USER_NAME					VARCHAR(32)		NOT NULL UNIQUE,
    	PASSWORD_AES				VARCHAR(416)	NOT NULL, 									-- 32
    	USER_DESC					VARCHAR(768)	NOT NULL,									-- 128 * 6 = 768
    	PRIMARY KEY ( USER_ID, USER_NAME ),
    	UNIQUE ( USER_ID )
    );
    GRANT SELECT, INSERT, UPDATE, DELETE ON LAB_FLASK.USER_LIST TO LAB_FLASK_ROLE;
    
    -- PRODUCT_LIST
    
    CREATE TABLE LAB_FLASK.PRODUCT_LIST (
    	PRODUCT_ID					CHAR(26)		NOT NULL UNIQUE,
    	PRODUCT_NAME				VARCHAR(32)		NOT NULL UNIQUE,
    	PRODUCT_DESC				VARCHAR(768)	NOT NULL,									-- 128 * 6 = 768
    	PRIMARY KEY ( PRODUCT_ID, PRODUCT_NAME )
    );
    GRANT SELECT, INSERT, UPDATE, DELETE ON LAB_FLASK.PRODUCT_LIST TO LAB_FLASK_ROLE;
    ```
* DML 文
    ```sql:lab-flask-dml.sql
    ・・・
    
    -- USER_LIST
    
    DELETE FROM LAB_FLASK.USER_LIST;
    INSERT INTO LAB_FLASK.USER_LIST
    	( USER_ID, USER_NAME, PASSWORD_AES, USER_DESC )
    	VALUES
    		( '2024-01-01T00:00:00.000000', 'root', HEX(AES_ENCRYPT('Asdf1234', 'Asdf1234Asdf1234')), 'Administrator' );
    
    -- PRODUCT_LIST
    
    DELETE FROM LAB_FLASK.PRODUCT_LIST;
    INSERT INTO LAB_FLASK.PRODUCT_LIST
    	( PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC )
    	VALUES
    		( '2024-01-01T00:00:00.000001', 'Apple', 'Made in Japan.' ),
    		( '2024-01-01T00:00:00.000002', 'Orange', 'Made in America.' );
    ```
    パスワードは暗号化（AES ECB）して保存しておきます。この .sql ファイルに暗号化のキー “Asdf1234Asdf1234” も含んでいます。この .sql ファイルは、データベースサーバー内のローカルで使用し、使用後は削除することを想定しています。

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

* メイン処理。変数 “FOLDER = 0” または “FOLDER = 1” で、Vue または React のリソースを切り替えてください。
    ```python:main.py
    ・・・
    
    import os
    from flask import Flask, render_template, send_from_directory, jsonify, request, make_response
    from flask_login import LoginManager, login_required, login_user, current_user, logout_user, UserMixin
    
    from action import auth_action, unauth_action, info_action
    
    
    FOLDERS = [
        {'static_folder':   './resources-react/static',
         'template_folder': './resources-react'},
        {'static_folder':   './resources-vue/assets',
         'template_folder': './resources-vue'},
    ]
    
    # Please select React environment or Vue environment.
    FOLDER = 0    # React
    # FOLDER = 1    # Vue
    
    app = Flask(__name__,
                static_folder=FOLDERS[FOLDER]['static_folder'],
                template_folder=FOLDERS[FOLDER]['template_folder'])
    app.config.from_pyfile("./settings.py")
    
    
    #
    def callback():
        json_data = {
            'status': 'action-ng',
            'message': 'Session timeout has occurred.',
            'list': []
        }
        return jsonify(json_data)
    
    
    login_manager = LoginManager()
    login_manager.unauthorized_callback = callback
    login_manager.init_app(app)
    
    
    #
    class User(UserMixin):
        def __init__(self, id_):
            self.id = id_
    
    
    #
    @login_manager.user_loader
    def load_user(user_id):
        return User(user_id)
    
    
    #
    @app.route('/', methods=['GET'])
    def index():
        response = make_response(render_template('index.html'))
        return response
    
    
    # When Manipulate [Rewind] [Forward] [Refresh] Button [URL] Textbox at Browser
    @app.errorhandler(404)
    def not_found(e):
        response = make_response(render_template('index.html'))
        return response
    
    
    #
    @app.after_request
    def after_request(response):
        response.headers['Cache-Control'] = 'private, no-store, no-cache, max-age=0, must-revalidate'
        response.headers['Expires'] = '0'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
        response.headers['X-Content-Type-Options'] = 'nosniff'
        response.headers['X-Frame-Options'] = 'SAMEORIGIN'
        response.headers['X-XSS-Protection'] = '1; mode=block'
        return response
    
    
    # favicon.ico
    @app.route('/favicon.ico', methods=['GET'])
    def favicon():
        return send_from_directory(os.path.join(app.root_path, FOLDERS[FOLDER]['template_folder']), 'favicon.ico')
    
    
    # Web API: authorize
    @app.route('/auth', methods=['POST'])
    def auth():
        return auth_action.auth(request, login_user)
    
    
    # Web API: un-authorize
    @app.route('/unauth', methods=['POST'])
    @login_required
    def unauth():
        return unauth_action.unauth(logout_user)
    
    
    # Web API: information
    @app.route('/info', methods=['GET'])
    @login_required
    def info():
        return info_action.info(request, current_user)
    
    
    # Web Server
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=8080)
    ```
    ブラウザーで “http://localhost:8080” をアクセスしたとき、index.html を表示する。
    また、ブラウザーで [戻る] ボタン、[進む] ボタン、[更新] ボタンを押下したり [URL] テキストボックスに “http://localhost:8080/FlaskLogin” 等を指定したりして 404（Not Found）になったときに index.html を表示することで、React ルーターに相応の処理を行わせる。  
* 設定ファイル。session の暗号化キー、CSRF 処理なし、レスポンス・ヘッダー “Set-Cookie:” のオプション（HttpOnly; Secure;）、タイムアウト（30分）。
    ```python:settings.py
    ・・・
    
    from datetime import timedelta

    SECRET_KEY = 'LabFlask'
    WTF_CSRF_ENABLED = False
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SECURE = True
    PERMANENT_SESSION_LIFETIME = timedelta(minutes=30)
    ```
* 認証処理。
    ```python:auth_action.py
    ・・・
    
    import binascii
    from flask import jsonify
    from flask_login import UserMixin
    from mapper import select_mapper
    from validator import auth_validator
    from Crypto.Cipher import AES
    
    # 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')
    
    
    #
    class User(UserMixin):
        def __init__(self, in_id):
            self.id = in_id
    
    
    # authorize
    def auth(request, login_user) -> jsonify:
    
        json_data = {
            'status': '',
            'message': '',
            'list': []
        }
    
        form = auth_validator.Form()
        if not form.validate_on_submit():
            json_data['status'] = 'action-ng'
            json_data['message'] = 'User Name or Password is not correct.'
            return jsonify(json_data)
    
        user_name = request.form['user_name']
        password = request.form['password']
    
        try:
            sql = 'SELECT PASSWORD_AES FROM LAB_FLASK.USER_LIST WHERE USER_NAME = %s'
            result = select_mapper.select(sql, [user_name])
        except Exception:  # noqa
            json_data['status'] = 'action-ng'
            json_data['message'] = 'Unknown trouble has occurred.'
            return jsonify(json_data)
    
        if not len(result):
            json_data['status'] = 'action-ng'
            json_data['message'] = 'User Name or Password is not correct.'
            return jsonify(json_data)
    
        # Password Verification
        password_aes = result[0]['PASSWORD_AES']
        password_dec = decrypto(password_aes)
        if password != password_dec:
            json_data['status'] = 'action-ng'
            json_data['message'] = 'User Name or Password is not correct.'
            return jsonify(json_data)
    
        login_user(User(user_name))
    
        json_data['status'] = 'action-ok'
        json_data['message'] = ''
        return jsonify(json_data)
    ```
    パスワードの復号化は、ココでやっています。SQL 文側でパスワードを復号化するのはセキュリティ的に望ましくないとされているためです。
* バリデーション処理。
    ```python:auth_validator.py
    ・・・
    
    import re
    
    from flask_wtf import FlaskForm
    from wtforms import StringField, ValidationError
    
    
    #
    class Form(FlaskForm):
    
        user_name = StringField('')
        password = StringField('')
    
        def validate_user_name(self, user_name) -> None:  # noqa
            if user_name.data == '':
                raise ValidationError('User ID or Password is not correct.')
            if len(user_name.data) > 32:
                raise ValidationError('User ID or Password is not correct.')
            if re.fullmatch("[a-zA-Z0-9_-]*", user_name.data) is None:
                raise ValidationError('User ID or Password is not correct.')
            return
    
        # Password verification is implemented in auth_action.py
        def validate_password(self, password) -> None:  # noqa
            if password.data == '':
                raise ValidationError('User ID or Password is not correct.')
            return
    ```
* データベースアクセス処理。
    ```python:select_mapper.py
    ・・・
    
    import mysql.connector
    
    
    #
    def select(sql, in_params):
        try:
            db_host = 'localhost'
            db_port = '3306'
            db_name = 'LAB_FLASK'
            db_user = 'LAB_FLASK_USER'
            db_password = 'Asdf1234'
            connect = mysql.connector.connect(
                host=db_host, port=db_port, db=db_name, user=db_user, passwd=db_password, charset="utf8")
            cursor = connect.cursor(dictionary=True)
            cursor.execute(sql, in_params)
            result = cursor.fetchall()
            connect.commit()
            connect.close()
        except Exception as ex:
            raise Exception(ex)
    
        return result
    ```
    MySQL コネクターを使用しています。今回は、コネクションプールは使用していません。

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

* ログイン画面 “FlaskLogin” の動作
  * axios を使用して Web API “/auth” を呼び出す。
  * 認証に成功した場合、ログイン後のホーム画面 “FlaskHome” に遷移する。
  * 認証に失敗した場合、ログイン画面のメッセージ行にエラーメッセージを表示する。
* React ログイン画面。
    ```tsx:FlaskLogin.tsx
    import 'bootstrap/dist/css/bootstrap.min.css';
    
    import {useEffect} from 'react';
    import {useState} from 'react';
    import {useSelector, useDispatch} from 'react-redux';
    import {mapStateToProps, mapDispatchToProps} from '../store/reducer';
    import {connect} from 'react-redux';
    
    import {Navbar} from 'react-bootstrap';
    import {Form, Button} from 'react-bootstrap';
    import {Container, Row, Col} from 'react-bootstrap';
    
    import {useNavigate} from "react-router-dom";
    import axios from 'axios';
    
    var styles = {
      margin: {
        margin: "55pt 10pt 10pt 10pt",
      },
      white: {
        color: "white",
      }
    };
    
    function FlaskLogin() {
      const [userName, setUserName] = useState('');
      const [password, setPassword] = useState('');
      const [message, setMessage] = useState('');
      const navigate = useNavigate();
      const sessionId = useSelector((state: {sessionId: string}) => state.sessionId);
      const dispatch = useDispatch();
      useEffect(() => {
        if (sessionId !== '') {
          dispatch({type: 'UPDATE', payload: ''});
          const formData = new FormData();
          axios.post('/unauth', formData);
        }
      }, []);  
      const login = async () => {
        if (! userName) {
          setMessage('User Name is required.');
          return	
        }
        if (! password) {
          setMessage('Password is required.');
          return
        }  
        const formData = new FormData();
        formData.append('user_name', userName);
        formData.append('password', password);
        await axios.post('/auth', formData)
        .then (function(response) {
          if (response.data.status === 'action-ok') {
            dispatch({type: 'UPDATE', payload: 'in session'});
            navigate('/FlaskHome');
            return;
          }
          else {
            setMessage(response.data.message);
            return;
          }
        })
        .catch (function(error) {
          setMessage('Network trouble has occurred.');
          return
        });
      };
      return (
        <>
          <header>
            <Navbar expand="lg" className="fixed-top navbar navbar-expand-lg px-lg-3 navbar-dark bg-primary">
              <Navbar.Brand><b>lab-flask</b></Navbar.Brand>
            </Navbar>
          </header>
          <footer className="footer fixed-bottom mt-auto p-lg-2 navbar-dark bg-primary">
            <span style={styles.white}>Copyright &copy; Xxxx Co., Ltd.</span>
          </footer>
          <main style={styles.margin}>
            <h5>Login</h5>
            <Container fluid="true">
              <Row noGutters="true">
                <Col>{message}</Col>
              </Row>
              <Row noGutters="true">
                <Col xs={12} md={4}>
                  <Form.Control type="text" value={userName} id="userName" placeholder="User Name" onChange={(e) => setUserName(e.target.value)} />
                </Col>
              </Row>
              <Row noGutters="true">
                <Col xs={12} md={4}>
                  <Form.Control type="password" value={password} id="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
                </Col>
              </Row>
              <Row noGutters="true">
                <Col md="auto">
                  <Button variant="outline-success" id="login" onClick={login}>Login</Button>
                </Col>
              </Row>
            </Container>
          </main>
        </>
      );
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(FlaskLogin);
    ```
* Vue ログイン画面。
    ```vue:FlaskLogin.vue
    <template>
      <v-app>
        <v-app-bar color="blue" app dark>
          <v-toolbar-title><h3 class="display-1">lab-flask</h3></v-toolbar-title>
        </v-app-bar>
        <v-footer color="blue" app dark>Copyright &copy; Xxxx Co., Ltd.</v-footer>
        <v-main>
          <v-container fluid>
            <v-row no-gutters>
              <v-col><h3 class="display-1">Login</h3></v-col>
            </v-row>
            <v-row no-gutters>
              <v-col>{{ message }}</v-col>
            </v-row>
            <v-row>
              <v-col cols="4">
                <v-form>
                  <v-text-field id="userName" autocomplete="off" prepend-icon="mdi-account" clearable label="User Name" v-model="userName" />
                  <v-text-field id="password"
                    autocomplete="off" prepend-icon="mdi-lock" clearable label="Password" v-model="password"
                    v-bind:type="showPassword ? 'text' : 'password'"
                    v-bind:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
                    @click:append="showPassword = ! showPassword" />
                  <v-btn id="login" variant="outlined" color="success" @click="login" style="text-transform: none">Login</v-btn>
                </v-form>
              </v-col>
            </v-row>
          </v-container>
        </v-main>
      </v-app>
    </template>
    
    <script setup>
    
    import axios from 'axios'
    
    import { ref } from 'vue'
    import { useRouter } from 'vue-router'
    import { useStore } from '../stores/store'
    
    const router = useRouter()
    const store = useStore()
    
    const message = ref('')
    const showPassword = ref(false)
    const userName = ref('')
    const password = ref('')
    
    if (store.sessionId != '') {
      const formData = new FormData()
      axios.post('/unauth', formData)
      store.sessionId = ''
    }
    
    function login() {
      if (! userName.value) {
        message.value = 'User Name is required.'
        return  
      }
      if (! password.value) {
        message.value = 'Password is required.'
        return
      }
      const formData = new FormData()
      formData.append('user_name', userName.value)
      formData.append('password', password.value)
      axios.post('/auth', formData)
      .then (function(response) {
        if (response.data.status == 'action-ok') {
          store.sessionId = "in session"
          router.push('/FlaskHome')
          return
        }
        else {
          message.value = response.data.message
          return
        }
      })
      .catch (function(error) {
        message.value = 'Network trouble has occurred.'
        return
      })
    }
    
    </script>
    ```

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

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

## 参考

**React と Vue.js の比較**

https://qiita.com/gaeul0228/items/c03c81fe0f2111220e24

https://qiita.com/kouphasi/items/bad24618a84d753da6cd

https://qiita.com/yasusun/items/a7a0f36e0b0df60d6308

https://qiita.com/kyrieleison/items/39ce30dd2d204791a9ea

**Vue から React への乗り換え**

https://fwywd.com/tech/nuxt-to-next

**Flask と FastAPI**

https://it-kiso.com/tech/development/fastapi-%e3%81%a8-flask-%e3%81%a9%e3%81%a1%e3%82%89%e3%81%8c%e3%81%82%e3%81%aa%e3%81%9f%e3%81%ab%e9%81%a9%e3%81%97%e3%81%a6%e3%81%84%e3%81%be%e3%81%99%e3%81%8b/

https://zenn.dev/nameless_sn/articles/flask_vs_fastapi

https://qiita.com/bee2/items/0ad260ab9835a2087dae

**React Router**

https://qiita.com/himuusagi/items/fc94288fc46ae3b2b7f8

https://qiita.com/mugrow/items/3d89e836b29c0b3d2534

**画面設計（いつか実践してみたい）**

https://qiita.com/mskmiki/items/544149987475719e417b