StreamlitとSupabaseで基礎的な認証アプリを作ってみた

## はじめに
「認証機能付きのウェブアプリを作るのは難しそう...」そう思っていませんか？

しかし、StreamlitとSupabaseを組み合わせることで簡単に実装できます。

本記事では、StreamlitとSupabaseを使用して、効率的に認証システムを構築する手順をご紹介します。


<details><summary>コード全文はこちら</summary>


```python 
import os
import streamlit as st
from supabase import create_client

url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_KEY")
supabase = create_client(url, key)

def sign_up(email, password):
    res = supabase.auth.sign_up({"email": email, "password": password})
    return res

def sign_in(email, password):
    res = supabase.auth.sign_in_with_password({"email": email, "password": password})
    return res

def sign_out():
    supabase.auth.sign_out()
    st.session_state.clear()

def check_auth():
    return 'user' in st.session_state

def login_signup_page():
    st.title("ログイン / サインアップ")
    tab1, tab2 = st.tabs(["ログイン", "サインアップ"])
    
    with tab1:
        email = st.text_input("メールアドレス", key="login_email")
        password = st.text_input("パスワード", type="password", key="login_password")
        if st.button("ログイン"):
            try:
                res = sign_in(email, password)
                st.session_state.user = res.user
                st.success("ログインに成功しました")
                st.experimental_rerun()
            except Exception as e:
                st.error(f"ログインに失敗しました: {str(e)}")

    with tab2:
        new_email = st.text_input("メールアドレス", key="signup_email")
        new_password = st.text_input("パスワード", type="password", key="signup_password")
        if st.button("サインアップ"):
            try:
                res = sign_up(new_email, new_password)
                st.success("アカウントが作成されました。メールを確認してアカウントを有効化してください。")
            except Exception as e:
                st.error(f"サインアップに失敗しました: {str(e)}")

def main_app():
    st.title("メインアプリケーション")
    st.write(f"ようこそ、{st.session_state.user.email}さん！")

    menu = ["ホーム", "コンテンツ",]
    choice = st.sidebar.selectbox("メニュー", menu)

    if choice == "ホーム":
        st.subheader("ホーム")
        st.write("ホームです。")

    elif choice == "コンテンツ":
        st.subheader("コンテンツ")
        st.write("ここにコンテンツを表示できます。")


    if st.sidebar.button("ログアウト"):
        sign_out()
        st.experimental_rerun()

def main():
    if not check_auth():
        login_signup_page()
    else:
        main_app()

if __name__ == "__main__":
    main()
```
</details>

## 事前準備
### ライブラリのインストール
まず必要なライブラリをインストールします。
```bash 
(venv)$ pip install streamlit supabase 
```

### Supabaseのセットアップ

Supabaseのセットアップは以下3つの手順で行います。

https://supabase.com/

1. 上記URLにアクセスしてSupabaseでアカウントを作成
2. 新しいプロジェクトを作成し、プロジェクトのURLとAPIキーを取得
3. プロジェクトのURLとAPIキーを環境変数に登録

環境変数の登録方法は以下を参考にしてください。
```bash 
$ export SUPABASE_URL="XXX"
$ export SUPABASE_KEY="XXX"
```

## プログラム
### 1.初期設定
まず、必要なライブラリをインポートし、Supabaseクライアントを初期化します。

```python 
import os 
import streamlit as st
from supabase import create_client

url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_KEY")
supabase = create_client(url, key)
```

### 2. 認証関数
次に、ユーザー認証に必要な関数を定義します。

```python 
def sign_up(email, password):
    return supabase.auth.sign_up({"email": email, "password": password})

def sign_in(email, password):
    return supabase.auth.sign_in_with_password({"email": email, "password": password})

def sign_out():
    supabase.auth.sign_out()
    st.session_state.clear()
```

これらの関数は、Supabaseの認証APIを利用して、サインアップ、サインイン、サインアウトの処理を行います。
また後述しますが、今回streamlitのセッション情報にログイン情報を書き込むので、サインアウト時に`st.session_state.clear()`を行い、セッションをクリアします。

### 3. ログイン/サインアップページ
ユーザーインターフェースの中心となるログイン/サインアップページを定義します。

```python 
def login_signup_page():
    st.title("ログイン / サインアップ")
    tab1, tab2 = st.tabs(["ログイン", "サインアップ"])
    
    with tab1:
        email = st.text_input("メールアドレス", key="login_email")
        password = st.text_input("パスワード", type="password", key="login_password")
        if st.button("ログイン"):
            try:
                res = sign_in(email, password)
                st.session_state.user = res.user
                st.success("ログインに成功しました")
                st.experimental_rerun()
            except Exception as e:
                st.error(f"ログインに失敗しました: {str(e)}")

    with tab2:
        new_email = st.text_input("メールアドレス", key="signup_email")
        new_password = st.text_input("パスワード", type="password", key="signup_password")
        if st.button("サインアップ"):
            try:
                res = sign_up(new_email, new_password)
                st.success("アカウントが作成されました。メールを確認してアカウントを有効化してください。")
            except Exception as e:
                st.error(f"サインアップに失敗しました: {str(e)}")
```

このページでは、Streamlitのst.tabsを使用してログインとサインアップの2つのタブを作成しています。各タブには入力フィールドとボタンがあり、ユーザーが簡単に操作できるようになっています。

![ 2024-08-27 0.33.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/3e1b4619-7a24-2c7d-bb8f-65bafcdfbdc8.png)

ログイン時に`st.session_state.user = res.user`でセッション情報を書き込みます。
これにより、ログインしているかどうかを後述のプログラムで判定できるようになります。


### 4.ログイン後のページ

ログイン後のページの例は以下です。

```python 
def main_app():
    st.title("メインアプリケーション")
    st.write(f"ようこそ、{st.session_state.user.email}さん！")

    menu = ["ホーム", "コンテンツ",]
    choice = st.sidebar.selectbox("メニュー", menu)

    if choice == "ホーム":
        st.subheader("ホーム")
        st.write("ホームです。")

    elif choice == "コンテンツ":
        st.subheader("コンテンツ")
        st.write("ここにコンテンツを表示できます。")


    if st.sidebar.button("ログアウト"):
        sign_out()
        st.experimental_rerun()
```

実験のためにログアウトボタンを実装しておくと良いでしょう。（本プログラムはサイドバーで実装）

![ 2024-08-27 0.25.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/28316538-8555-55ca-3f79-bbf8f85693cc.png)

### 5. 認証チェックと全体の流れ制御
最後に、アプリケーション全体の流れを制御する関数を定義します。

```python 
def check_auth():
    return 'user' in st.session_state


def main():
    if not check_auth():
        login_signup_page()
    else:
        main_app()

if __name__ == "__main__":
    main()
```

`check_auth()`は、ユーザーが認証されているかどうかをチェックします。
main関数は、認証状態に基づいて適切なページを表示します。




## まとめ
このように、StreamlitとSupabaseを組み合わせることで簡単に認証アプリを作成できます。

自身でログイン処理をフルスクラッチで実装するのに比べて、簡単に実装できて良いですね。