【Flask】サインアップ・サインイン機能を実装する方法

# 1.概要
Flaskでサインアップとサインイン機能を実装する方法の手順です。
また、基礎的なFlaskの知識やWebアプリの知識なども一部紹介しております。

# 2. 注意事項
学生時代、ご学友向けに作成した手順書を一部改変し、この記事を作成いたしました。
Flaskの基礎的な文法を勉強した後、我流で実装してみたものになります。
その為、誤った解釈や不正確な情報が含まれている可能性があります。
あくまで参考程度にご覧いただくことをおすすめいたします。
なお、ソースコードは以下のリンクのprogramフォルダから閲覧可能です。

https://github.com/hosoya17/login

# 実装内容
- サインアップ(新規登録)機能
- サインイン(ログイン)機能

# 前提
事前にPython3をインストールしてください。

# 開発環境
- OS
    - Windows11
- 使用言語
    - Python3.12.3
- データベース
    - SQLite

# 環境構築
## ライブラリのインストール
事前にフレームワーク、ライブラリをインストールする必要があります。インストール方法は以下の通りです。
```Shell
pip install flask, sqlite3
```
## ファイル作成
任意のドライブやフォルダに以下の構造でフォルダとファイルを作成してください。
```
flaskr/
    ├── templates/
    │   ├── add.html
    │   ├── check.html
    │   ├── comp.html
    │   ├── index.html
    │   └── top.html
    ├── __init__.py
    ├── db.py
    └── main.py
```

# プログラム解説
## 1. プログラム実行時に処理されるプログラム
### \_\_init__.py
```Python
from flask import Flask
app = Flask(__name__)
import flaskr.main
```

## 2. プログラム実行時にindex.htmlを表示する
### main.py
```Python
import re
import hashlib
import sqlite3
from flaskr import app
from flask import render_template, request, redirect, url_for, session

@app.route('/')
def index():
    return render_template(
        'index.html'
    )
```

ここまで書いたら実行してindex.htmlにアクセスしてみましょう。
実行方法はCMDもしくはターミナル(Macの場合)に以下のコマンドを入力してください。
なお、コマンドは必ず1行ずつ実行してください。

### コマンド
#### Windows(CMD)環境
```Shell
set FLASK_APP=flaskr
set FLASK_ENV=development
set FLASK_DEBUG=1
python -m flask run
```
#### Mac(Bash)環境
```Shell
export FLASK_APP=flaskr
export FLASK_ENV=development
export FLASK_DEBUG=1
python -m flask run
```

実行できたらブラウザに`localhost:5000`と入力しアクセスしてください。
まっさらなページが表示されたら成功です。

## 3. index.htmlからadd.html(新規登録画面)に遷移する
### main.py
先程記述したindex関数とほぼ同じです。
なお、render_template関数は画面遷移する時に使用する関数です。
main.pyの下部に以下を追記してください。
```Python
@app.route('/add')
def add():
    return render_template(
        'add.html'
    )
```

### index.html
{{}}を使用するとhtmlにPythonのコードを埋め込めるようになります。
```HTML
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>ログイン</title>
</head>
<body>
 <form action="" method="">
  <input type="text" name="userID">
  <input type="password" name="password">
  <input type="submit" value="ログイン" class="loginBtn">
 </form>
 <!-- 以下のaタグのhref属性が重要 -->
 <a href="{{ url_for('add') }}">新規登録はこちら</a>
</body>
</html>
```

ブラウザを更新し、index.htmlからadd.html(現在はまっさらなページ)にアクセスできるかテストしてみてください。

## 4. add.htmlのinput要素(テキストボックスなど)に入力された値を取得しcheck.html(確認画面)に表示する
### add.html
```html:add.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>新規登録</title>
</head>
<body>
 
 <!-- 必ずname属性を記述してください。 -->
 <form method="post" action="{{ url_for('check') }}">
  <input type="text" name="userID">
  <input type="password" name="password1">
  <input type="password" name="password2">
  <input type="text" name="mail">
  <input type="submit" value="確認">
 </form>

</body>
</html>
```

### main.py
main.pyの下部に以下を追記してください。
```Python:main.py
@app.route('/check', methods=['POST'])
def check():
  userID = request.form['userID']
  password1 = request.form['password1']
  password2 = request.form['password2']
  mail = request.form['mail']

  return render_template(
    'check.html',
    userID=userID,
    password1=password1,
    mail=mail
  )
```

#### プログラムの解説
`@app.route`の()内に`methods=['POST']`と記述することにより、POSTで通信することが可能になります。

input要素に入力された値は`request.form['htmlで指定したname属性']`で取得できます。

遷移先の画面へ値を渡す際は`render_template関数`の中に`変数名=値`と記述すれば渡せます。なお、変数名と値は同じ名前にすることを推奨いたします。

### check.html
先程、`check関数`の`render_template関数`で渡した変数は{{}}に変数名を記述すればhtmlで表示できます。
```html:check.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>確認画面</title>
</head>
<body>

<p>{{ userID }}</p>
<p>{{ password1 }}</p>
<p>{{ mail }}</p>

 <form action="" method="">
  <input type="submit" value="登録">
 </form>

</body>
</html>
```

### 補足(POSTとGETの使い分けについて)
一先ず、input要素で入力された値を取得、保存する時の通信方法はPOST、そうでない時(画面遷移など)はGET、と覚えておく程度で良いと思います。
詳しく説明すると記事1つ分くらいの分量になってしまう為、詳しく知りたい方はググるかITパスポートや基本情報技術者などの参考書を参照してください。

## 5. セキュリティについて
### GETで通信された場合
現状、ブラウザのurl欄にcheck.htmlのurl(localhost:5000/check)を入力するとエラーが表示されてしまいます。
実際にクラッカーなどはこのエラーメッセージを読み攻撃手法を考えるらしいので、表示されないようにします。

### main.py
main.pyのcheck関数に以下を追記してください。
```Python:main.py
@app.route('/check', methods=['POST', 'GET']) # 敢えて、GETでの通信も許可してやる。
def check():
  if request.method == 'POST':
    userID = request.form['userID']
    # 以下略
  else:
    return redirect(
      url_for('index')
    )
```

#### プログラムの解説
url欄に直接urlを入力してアクセスすると、通信方法はGETになる為、通信方法がGETの場合index.htmlにリダイレクトさせます。

`@app.route`の`methods=['POST']`に`'GET'`を追加し、敢えてGETでの通信も許可します。

通信方法は`request.method`で取得できます。
if文で通信方法が`POST`だったら`check.html`に遷移させ、`GET`だったら`index.html`にリダイレクトするように処理を記述します。

なお、リダイレクトは`redirect関数`と`url_for関数`を使用します。
ここで間違いやすいのが、url_for関数では拡張子(.html)は不要です。

### 未入力チェック
現状、何も入力していなくてもcheck.htmlに遷移してしまいます。
空のデータがデータベースに保存されてしまうのはまずいので何とかします。
入力されていないデータがある場合はadd.htmlに遷移し、エラーを表示するようにします。

#### main.py
main.pyのcheck関数に以下を追記してください。
```Python:main.py
# 上部省略
mail = request.form['mail']
if userID and password1 and password2 and mail:
  return render_template(
    'check.html',
    userID=userID,
    password1=password1,
    mail=mail
  )
else:
  error = "全ての項目を入力してください"
  return render_template(
    'add.html',
    error=error
  )
    # 以下略
```

#### add.html
add.htmlのformタグの上にif文を追記してください。
```HTML:add.html
{% if error %}
  <p>{{ error }}</p>
{% endif %}
<form method="post" action="{{ url_for('check') }}">
  <input type="text" name="userID">
  <!-- 以下略 -->
```

##### プログラムの解説
単純にif文で変数に値が入っていないかをチェックしています。
なぜ上記の条件式でできるのか分からない方は、`Python falsy`で検索してください。

### パスワード、メールアドレスの形式チェック
現状、パスワードとメールアドレスは何を入力してもcheck.htmlに遷移してしまいます。
パスワードに関しては、強度の強いパスワードを設定させるようにするのが望ましいです。
その為、強度が弱いパスワードを入力されたらエラーを表示するようにします。
また、確認用で同じパスワードを入力させ比較し、違うものが入力されていたらエラーを表示するようにします。

メールアドレスに関しては正しい形式でなければエラーを表示するようにします。

#### main.py
main.pyのcheck関数に以下を追記してください。
```Python:main.py
mail = request.form['mail']

password_pattern = r'^(?=.*[0-9a-zA-Z\W]).{6,}$'
mail_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
if userID and password1 and password2 and mail and password1 == password2 and re.match(password_pattern, password1) and re.match(mail_pattern, mail):
  # 省略
elif password1 != password2:
  error = "入力されたパスワードと確認用のパスワードが異なります"
elif not(userID and password1 and password2 and mail):
  error = "全ての項目を入力してください"
elif not(re.match(password_pattern, password1)):
  error = "パスワードは次の条件を満たしている必要があります。半角英数字記号を使用、6文字以上"
elif not(re.match(mail_pattern, mail)):
  error = "正しいメールアドレスの形式ではありません。"
# if文の外に記述するしてください
return render_template(
  'add.html',
    error=error
  )
# 以下略
```

##### プログラムの解説
パスワード、メールアドレス共に、if文で正規表現と比較しています。
なお、パスワードは6文字以上半角英数字記号を使用できるという正規表現にしております。

### パスワードの表示について
現状、画面遷移先で入力されたパスワードがそのまま表示されるようになっています。
セキュリティ的によろしくない為、●などで伏せて表示されるようにします。

#### check.html
check.htmlで乗算やlengthを使用し、パスワードの文字数分●を表示するようにします。
```html:check.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>確認画面</title>
</head>
<body>

<p>{{ userID }}</p>
<!-- 下部を変更 -->
<p>{{ '●' * password|length }}</p>
<p>{{ mail }}</p>

 <form action="" method="">
  <input type="submit" value="登録">
 </form>

</body>
</html>
```

#### 補足
このやり方もパスワードの文字数が分かってしまう為、万全なセキュリティ対策とは言えません。
もしこれよりもセキュリティを万全にしたい場合は各自で調べて下さい(丸投げ)。

## 7. データベース作成

### 概要

前述したようにデータベースはSQLiteを使用します。
ユーザーテーブルを作成します。テーブル定義は以下の通りです。

|No.|カラム名|データ型|Not Null|デフォルト|備考|
|----|----|----|----|----|----|
|1|userID|VARCHAR|NOT NULL||ユーザーのID|
|2|hashed_password|VARCHAR|NOT NULL||ユーザーのハッシュ化されたパスワード|
|3|mail|VARCHAR|NOT NULL||ユーザーのメールアドレス|

### db.py
```Python
import sqlite3

#データベース名はお好きな名前で構いません
DATABASE = 'データベース名.db'

def create_expenses_table():
  con = sqlite3.connect(DATABASE)
  con.execute("CREATE TABLE IF NOT EXISTS user(userID VARCHAR PRIMARY KEY, hashed_password VARCHAR NOT NULL, mail VARCHAR NOT NULL)")
  con.close()
```

#### プログラムの解説
`con = sqlite3.connect(DATABASE)`でデータベースと接続できます。
`con.execute("")`の""(ダブルクォート)の中にSQL文を記述します。
なお、''(シングルクォート)でも動作しますが、SQLの文法上''を使用することが多々ある為、""を使用することをおすすめします。
`con.close()`でデータベースとの接続を切断できます。
用事が済んだら必ず切断しましょう。

### \_\_init__.py
\_\_init__.pyの下部に以下を追記して、実行時にデータベースが作成されるようにします。
```Python
from flaskr import db
db.create_expenses_table()
```

### 補足(CREATE TABLE文の文法等々)
こちらの補足ではCREATE TABLE文の文法に関する説明が長々と続きます。
既にご存じの方は項目8まで飛ばして下さい。

#### 主キーについて
SQLで主キーを設定する際の文法、並びに主キーの条件は以下の通りです。
- `PRIMARY KEY`と記述する
- 重複を許さない
- NULL(空文字)を許さない
- 基本的に一番最初に記述する

#### 基本的な文法
CREATE TABLE文の基本的な文法です。
```SQL
CREATE TABLE テーブル名(カラム名1 型名 PRIMARY KEY, カラム名2 型名)
```

#### NOT NULLについて
主キー以外でNULLを許さない場合、NOT NULLと記述します。
```SQL
CREATE TABLE テーブル名(カラム名 型名 主キー, カラム名 型名 NOT NULL)
```

#### IF NOT EXISTSについて
PythonやPHPなどのプログラミング言語でデータベースを作成するプログラムを記述すると、毎回CREATE TABLE文が実行されてしまいます。
その為、2度目の実行は不要な上に、データが全て消えてしまう危険性がある為、テーブルが存在しない時だけCREATE TABLE文を実行するようにしたいです。
そこで、IF NOT EXISTSを使用すれば上記のことが解決できます。
```SQL
CREATE TABLE IF NOT EXISTS テーブル名(カラム名 型名 主キー, カラム名 型名 NOT NULL)
```

## 8. 入力されたデータをデータベースに保存する
### 概要
check.htmlからcomp.htmlに遷移するタイミングでデータベースにデータを保存します。
しかし現状、check.htmlからユーザーID、パスワードなどの情報を取得できません。
そこで、sessionを使用してcheck.htmlからもデータを取得できるようにします。

#### \_\_init__.py
まず、\_\_init__.pyでセッションキーを設定します。
また、セッションを保持する時間もこちらで定義します。
以下を追記してください。
```Python:\_\_init__.py
from flask import Flask
app = Flask(__name__)
import flaskr.main

# 以下を追記
import os
app.secret_key = os.urandom(24) # セッションキーの設定

from datetime import timedelta
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) # データを保持する時間の設定
# ここまで

from flaskr import db
db.create_expenses_table()
```

#### main.py
main.pyでセッションに保持しておきたいデータを辞書型で格納しておき、それをセッションで保持しておきます。
check関数に以下を追記してください。
```Python:main.py
if userID and password1 and password2 and mail and password1 == password2 and re.match(password_pattern, password1) and re.match(mail_pattern, mail):

  #以下から追記
  user = {
    'userID' : userID,
    'password' : password1,
    'mail' : mail
  }

  session['user'] = user

  return render_template(
    'check.html',
    user=user
  )
  # 以下略
```

#### check.html
check.htmlも一部書き方が変わります。
以下の様に変更してください。
```html:check.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>確認画面</title>
</head>
<body>

<!-- 下部から変更 -->
<p>{{ user.userID }}</p>
<p>{{ '●' * user.password|length }}</p>
<p>{{ user.mail }}</p>

 <form action="" method="">
  <input type="submit" value="登録">
 </form>

</body>
</html>
```

### データベースにデータを保存する
まず前提として、セキュリティ上、パスワードはハッシュ化して保存します。
pythonでは`hashlib.sha256(ハッシュ化したい物.encode()).hexdigest()`でハッシュ化できます。

#### main.py
main.pyに以下を追記してください。
```Python:main.py
# import文の下あたり
DATABASE = "データベース名.db"
# 省略

@app.route('/comp', methods=['POST', 'GET'])
def comp():
  if request.method == 'POST':
    user = session.get('user')

    hashed_password = hashlib.sha256(user['password'].encode()).hexdigest()

    con = sqlite3.connect(DATABASE)
    con.execute("INSERT INTO user VALUES(?, ?, ?)", (user['userID'], hashed_password, user['mail']))
    con.commit()
    con.close()

    return render_template(
      'comp.html'
    )
  else:
    return redirect(
      url_for('index')
    )
```

#### comp.html
```HTML:comp.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>登録完了</title>
</head>
<body>
 <p>ご登録ありがとうございます！</p>
</body>
</html>
```

#### 補足(INSERT文の文法等々)
INSERT文の基本的な文法の説明になります。
##### 基本的な文法
```SQL
INSERT INTO テーブル名(カラム名1, カラム名2) VALUE(値1, 値2);
```

## 9. ログイン処理
### POSTの場合
#### main.py
main.pyの下部に以下を追記してください。
```Python:main.py
@app.route('/top', methods=['GET', 'POST'])
def top():
  if request.method == 'POST':
    userID = request.form['userID']
    hashed_password = hashlib.sha256(request.form['password'].encode()).hexdigest()

    con = sqlite3.connect(DATABASE)
    existing = con.execute("SELECT * FROM user WHERE userID = ? AND hashed_password = ?", (userID, hashed_password)).fetchall()
    con.close()

  if existing:
    session['userID'] = userID
    return render_template(
      'top.html',
      userID=userID
    )
  else:
    error = 'ユーザーIDかパスワードが間違っています。'
    return render_template(
      'index.html',
      error=error
    )
```

### GETの場合
#### main.py
main.pyのtop関数の下部に以下を追記してください。
```Python
@app.route('/top', methods=['GET', 'POST'])
def top():
  if request.method == 'POST':
    # 省略
  else:
    if 'userID' in session:
      userID = session['userID']

      return render_template(
        'top.html',
        userID=userID
      )
    else:
      error = '長時間操作が行われなかったため、ログアウトしました。'
      return render_template(
        'index.html',
        error=error
      )
```

#### <font color="red">爆裂重要事項</font>
check関数やcomp関数では通信方法がGETだった場合、単純にindex.htmlにリダイレクトするだけで良かったです。
しかし、ログイン後に別のページからtop.htmlに遷移する可能性が高い為、GETでアクセスされた場合の処理も考えなければいけません。

上記のプログラムでは、セッションがあればログインしていると見做し、top.htmlへ遷移するようにしています。
対してセッションがない場合、セッションの有効期限が切れている、もしくは、直接url欄にtop.htmlのurlを打ち込んでアクセスされたと見做し、index.htmlに遷移し、`セッションが切れて自動的にログアウトした`とエラーメッセージを表示するようにしています。

なお、後者の場合も、セッションが切れて自動的にログアウトしたとエラーメッセージが表示されいますが、後者は大半が悪意のあるユーザーしか行わない行為であるため、同じエラーメッセージでも良しとしています。

### index.html
formタグのaction属性、method属性を忘れずに追記してください。
```HTML:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>ログイン</title>
</head>
<body>
 <!-- 忘れずに! -->
 <form action="{{ url_for('top') }}" method="post">
  <input type="text" name="userID">
  <input type="password" name="password">
  <input type="submit" value="ログイン" class="loginBtn">
 </form>
 <a href="{{ url_for('add') }}">新規登録はこちら</a>
</body>
</html>
```

### top.html
ただtop.htmlに遷移しただけでは本当にログインできているか分からない為、ユーザーIDを表示するようにします。
```HTML:top.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>トップ</title>
</head>
<body>
 <p>{{ userID }}</p>
</body>
</html>
```

##### プログラムの解説
ユーザーIDとパスワードの比較はSQLのSELECT文で行います。
ヒットすればユーザーIDとパスワードに誤りは無く、ヒットしなければどちらかが誤っている、もしくはユーザーIDが存在しないことになります。
なお、パスワードはハッシュ化して保存されている為、入力されたパスワードはハッシュ化してから比較しなければいけません。
比較した後、ユーザーIDとパスワードに誤りが<font color="red">無ければ</font>top.htmlへ遷移するようにしています。
対して、ユーザーIDとパスワードに誤りが<font color="red">あれば</font>index.htmlへ遷移してエラーメッセージを表示します。

### 補足(SELECT文の文法等々)
こちらの補足ではSELECT文についての説明が長々と続きます。
既にご存じの方は項目10まで飛ばして下さい。

#### 基本的な文法
```SQL
SELECT カラム名1, カラム名2 FROM テーブル名;
```

#### データを全て取得する場合
```SQL
SELECT * FROM テーブル名;
```

#### WHERE句
```SQL
SELECT * FROM テーブル名 WHERE カラム名 = 'example'; /* カラム名の値がexampleに一致するデータを取得する */
SELECT * FROM テーブル名 WHERE カラム名 <> 'example'; /* カラム名の値がexampleに一致しないデータを取得する */
SELECT * FROM テーブル名 WHERE カラム名 < 10; /* カラム名の値が10未満のデータを取得する */
SELECT * FROM テーブル名 WHERE カラム名 <= 10; /* カラム名の値が10以下のデータを取得する */
SELECT * FROM テーブル名 WHERE カラム名 > 10; /* カラム名の値が10より大きいデータを取得する */
SELECT * FROM テーブル名 WHERE カラム名 >= 10; /* カラム名の値が10以上のデータを取得する */
```

#### ORDER BY句
```SQL
SELECT * FROM テーブル名 ORDER BY カラム名 ASC; /* カラム名の値を昇順に並べて表示する(ASCは省略可) */
SELECT * FROM テーブル名 ORDER BY カラム名 DESC; /* カラム名の値を降順に並べて表示する */
```

### 補足(fetchall関数について)
fetchall関数ではSELECT文で取得したデータを全て変数に格納する関数です。
リスト形式でデータを取得します。
なお、最初の1件だけ取得したい場合はfetchone関数を使用すれば良いです。
```Python
data = con.execute("SELECT * FROM table").fetchall() # 取得したデータを全てdataに格納する
data = con.execute("SELECT * FROM table").fetchone() # 最初の1件だけdataに格納する
```

## 10. 既存のユーザーIDやメールアドレスを登録させない処理
### 概要
このアプリのデータベース(ユーザーテーブル)では、主キーがユーザーIDになっており、既存のユーザーIDを追加しようとするとエラーになります。
また、大規模SNSは例外として、メールアドレスも重複を許さないアプリが大半です。

### main.py
main.pyのcheck関数に以下を追記してください。
```Python:main.py
@app.route('/check', methods=['POST', 'GET'])
def check():
  if request.method == 'POST':
    # 省略

    con = sqlite3.connect(DATABASE)
    existing_user = con.execute("SELECT userID FROM user WHERE userID = ?", (userID,)).fetchone()
    existing_mail = con.execute("SELECT mail FROM user WHERE mail = ?", (mail,)).fetchone()
    con.close()

    if userID and password1 and password2 and mail and password1 == password2 and re.match(password_pattern, password1) and re.match(mail_pattern, mail) and not(existing_user) and not(existing_mail):

      # 省略

    elif password1 != password2:
      # 省略
    elif not(userID and password1 and password2 and mail):
      # 省略
    elif not(re.match(password_pattern, password1)):
      # 省略
    elif not(re.match(mail_pattern, mail)):
      # 省略
    elif existing_user:
      error = "ユーザーIDが既に存在します。別のユーザーIDを設定してください。"
    elif existing_mail:
      error = "このメールアドレスは既に登録されています。"

    return render_template(
      'add.html',
      error=error
    )
# 以下略
```

## 11. サインアウト
### 概要
セッションを破棄してindex.htmlにリダイレクトするという処理を記述します。
なお、セッションは`session.pop('破棄したいセッション', None)`で破棄できます。

### main.py
main.pyの下部に以下を追記してください。
```Python:main.py
@app.route('/logout')
def logout():
  session.pop('userID', None)
  return redirect(
      url_for('index')
  )
```

### top.html
top.htmlに以下を追記してください。
```HTML:top.html
<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>トップ</title>
</head>
<body>
 <p>{{ userID }}</p>
 <form action="{{ url_for('logout') }}">
  <input type="submit" value="ログアウト" class="logoutBtn">
 </form>
</body>
</html>
```

# おまけ
## 知っておいた方が良いこと
### データベースの操作方法
#### 概要
わざわざPythonで操作しなくても、コマンドプロンプトでデータベースを操作できます。
データの閲覧や、実装前にSQL文が正しく動作するかの確認などで使用すると非常に便利です。

#### 環境構築
手順が多い為、Googleで「SQLite パス」などで検索し、他の個人ブログなどから環境構築の方法を閲覧してください。

#### コマンドプロンプトで操作する
コマンドプロンプトで、データベースファイルがあるフォルダまで移動し以下のコマンドを実行してください。
```Shell
sqlite3 データベースファイル名.db
```

sqlite3との対話モードになり、この状態でSQL文を実行できます。
なお、デフォルトの状態だとカラム名が表示されず見辛いですが、以下のコマンドでカラム名を表示できるようになります。

```Shell
.mode column
```

終了する時は以下のコマンドを入力します。
```Shell
.exit
```

### Webアプリケーション開発でよく使うSQL文
#### 概要
サインアップ、サインイン機能では使わなかったSQL文の紹介になります。

#### UPDATE文
データを更新、編集する際に使用します。

##### <font color="red">爆裂重要事項</font>
Webアプリケーションの開発ではUPDATE文の文法は全て押さえていないと、ほぼ確でデータを壊すことになります。
UPDATE文に関する項目は基本的な文法だけでなく、必ず最後まで読むことを強く推奨いたします！

##### 基本的な文法
以下の例は該当するカラムの値を<font color="red">全て</font>書き換えるものです。
```SQL
UPDATE テーブル名 SET カラム名 = 値;
```

##### WHERE句で条件式をつける
以下の例はWHERE句で指定した条件式に該当するデータの値を書き換えるものです。
```SQL
UPDATE テーブル名 SET カラム名 = 値 WHERE カラム名 = "値";
```

#### DROP TABLE文
テーブルを削除する際に使用します。
頻繁に使うものではありませんが、開発中にテーブル定義が変更になったり、誤って挿入してはいけないデータを挿入してしまったりなど、テーブルを削除せざるを得ないことも多々あります。

##### 基本的な文法
```SQL
DROP TABLE テーブル名;
```

#### 補足(DELETE文について)
データを削除する際に使用するDELETE文ですが、Webアプリケーションの開発では使わないことが多いです。
実際にデータを削除する際は削除フラグを使用し、アプリ上で表示できなくしてデータベース上ではデータを保持しているケースが多いです。

## ファイル分割
### 概要
main.pyに全てのルーティングを記述するとコードの量が膨大になり可読性が低くなってしまいます。 
そこで、各機能ごとにファイルを分けることにより、可読性や保守性を高くすることができます。

### 例
- flaskrフォルダ直下にフォルダを作成する(作らなくても良いがファイルの数が多くなると保守性が低下するので作ることをおすすめする)
- 各ファイルに関数を作る
- main.pyでインポートする

以下はサインインとサインアップとサインアウトをファイル分割したものになります。  
なお、各関数の中身は以下のGitHubのファイル分割後というフォルダを参照してください。

https://github.com/hosoya17/login

### sing_up.py
```Python
import re
import hashlib
import sqlite3
from flaskr import app
from flask import render_template, request, redirect, url_for, session

def sing_up(app):
  # 関数の中身はGitHubのファイル分割後フォルダ参照
```

### sing_in.py
```Python
import re
import hashlib
import sqlite3
from flaskr import app
from flask import render_template, request, redirect, url_for, session

def sing_in(app):
  # 関数の中身はGitHubのファイル分割後フォルダ参照
```

### sing_out.py
```Python
import hashlib
from flaskr import app
from flask import redirect, url_for, session

def sing_out(app):
  # 関数の中身はGitHubのファイル分割後フォルダ参照
```

### main.py
```Python
import re
import hashlib
import sqlite3
from flaskr import app
from flask import render_template, request, redirect, url_for, session

from flaskr.routes.sing_up import sing_up
from flaskr.routes.sing_in import sing_in
from flaskr.routes.sing_out import sing_out

@app.route('/')
def index():
  return render_template(
    'index.html'
  )
```

## sqlalchemyについて
### 概要
sqlalchemyというSQL文を記述せずにデータベースを操作できるライブラリです。

### メリット
- SQL未学習の人、苦手な人は感覚的に操作できて扱いやすい(かもしれない)

### デメリット
- SQLが得意な人、慣れている人はsqlalchemyの独特な文法に戸惑う人が多い(気がする)
- Linuxなどの一部環境では正しく動作しないこともある

## flask-loginについて
### 概要
flask-loginというライブラリを使用すると、ユーザ情報の取得の簡易化や、認証が必要なroute管理などがやりやすくなります。
大多数の個人ブログではflask-loginを使用してログイン機能を実装している方がほとんどです。

しかし、ログイン画面の作成、ログイン可能かどうかなどの処理は自分で作成しなければいけません。
つまり今回の場合、flask-loginを使用しても、しなくてもさほど変わりません。
また、認証情報の保持がSessionである為、JWTなどはflask-loginは対象外です。

と、長々と説明してみましたが、本当の理由は勉強するのが面倒だk...