ReactとDjangoでログイン機能を実装する

こんにちは、ジョンです。
フロントをReact、サーバーサイドをDjangoで開発する場合のログイン機能実装方法を記します。

:::note info
以下、バージョン関係の情報です。

Django：4.2.8

React：^18.2.0",

OS：mac OS
:::

# 開発環境の構築
## まずはDjangoの環境を構築します。
プロジェクト名とアプリ名は任意の名前でOKです。
私はcoreとwebsiteという名前にしました。
```bash:terminal
python3 -m venv venv

source venv/bin/activate

python -m pip install --upgrade pip

pip install django
pip install djangorestframework
pip install django-cors-headers

pip freeze > requirements.txt

django-admin startproject core .

python manage.py startapp website
```

djangorestframeworkはリアクトとDjangoがAPIでの通信をするのに必要です。

django-cors-headersは、Reactからのリクエストがブロックされる
可能性を防ぐために、CORSヘッダーを設定するのに必要です。

## Djangoの設定ファイル編集
```python:settings.py
INSTALLED_APPS = [
    # --------------------
    'website',# ←追加
    'rest_framework',# ←追加
    'corsheaders',# ←追加
]

MIDDLEWARE = [
    # --------------------
    'corsheaders.middleware.CorsMiddleware', # ←追加
]

# 以下追加
CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000',  # ReactアプリのURL
]
```

## Reactのインストール
DjangoのルートディレクトリでReactをインストールします。
アプリ名は任意の名前でOKです。
```bash:terminal
npx create-react-app myapp
npm install react-router-dom # Reactでルーティング設定するのに必要
```

myapp ディレクトリ内のpackage.jsonにプロキシ設定を追加して、
APIリクエストをDjangoサーバーに転送できるようにする。

```json:package.json
{
  "name": "myapp",
  
      ----省略----
  
  "browserslist": {
  
      ----省略----
  
  },
  "proxy": "http://localhost:8000"
}

```

ここまでの作業で問題ないかを確認するため、DjangoとReactでサーバーを立ち上げ、
エラーがないかを確認してください。

```bash:terminal
# Djangoのサーバー起動
# http://localhost:8000にアクセスできるか
python manage.py runserver

# Reactのサーバー起動
# http://localhost:3000にアクセスできるか
cd myapp
npm start
```


# DjangoでAPIを受け取る処理を実装
Reactからのログインに関するリクエストを受け取った時の処理を記述します。

## URLの設定
api/以降のリクエストはwebsiteのurlsを参照するように設定。

```python:core/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('api/', include('website.urls')),
    path('admin/', admin.site.urls),
]
```

まだlogin_viewは作成していないので、以下のコードを保存するとエラーが
出るかもですが、最終的には以下のURLになります。
```python:website/urls.py
from django.urls import path
from .views.login_views import login_view

urlpatterns = [
    path('login/', login_view, name='login'),
]
```

## モデルの作成
今回はDjangoがデフォルトで用意しているUserモデルを使用するので
モデルの定義は省略します。（モデルを作成する必要ないです）

## views作成

```python:website/views/login_views.py
from django.contrib.auth import authenticate
from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(['POST'])
def login_view(request):
    username = request.data.get('username')
    password = request.data.get('password')
    user = authenticate(username=username, password=password)
    if user is not None:
        # ユーザーが存在する場合の処理
        return Response({'message': 'Login successful'})
    else:
        # ユーザーが存在しない場合の処理
        return Response({'message': 'Invalid credentials'}, status=400)
```

- @api_view(['POST'])はDjango REST framework (DRF) におけるデコレーターの一つ
- POSTリクエストが行われた場合にのみ、関数が実行されます
- 他のHTTPメソッドでリクエストが行われた場合、Django REST frameworkは405 Method Not Allowedというレスポンスを返します
- usernameとpasswordにはログインフォームに入力された値が格納されます。
- POST リクエストのボディ内のデータのキー（フィールド名）と一致している必要があります。
- authenticateはDjangoがもともと要している関数で、Userモデルにリクエストされたusernameとpasswordが存在するかを探してくれます。
- Djangoが用意したUserモデルを使用しない場合は修正する必要があります

# Reactでログインフォームを送信する機能を実装

## LoginFormの作成
src/ディレクトリ内にcomponentsディレクトリを作成し、その中に
LoginForm.jsを作成します。
```react:LoginForm.js
import React, { useState } from 'react';
import './LoginForm.css';

function LoginForm() {
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();
        const response = await fetch('/api/login/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ username, password }),
        });
        const data = await response.json();
        if (response.status === 200) {
            alert('Login successful');
        } else {
            alert(data.message);
        }
    };

    return (
        <div className="login-container">
            <form className="login-form" onSubmit={handleSubmit}>
                <input
                    type="text"
                    className="login-input"
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                    placeholder="Username"
                />
                <input
                    type="password"
                    className="login-input"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    placeholder="Password"
                />
                <button type="submit" className="login-button">Login</button>
            </form>
        </div>
    );
}

export default LoginForm;

```

## App.jsでルーティング設定
上記で作成したLoginFormをipmortして描画できるようにします。

```react:src:App.js
import logo from './logo.svg';
import './App.css';
import React from 'react';
import LoginForm from './components/LoginForm';

function App() {
  return (
      <div>
          <LoginForm />
      </div>
  );
}

export default App;
```

## ReactでLoginForm.jsが描画されるまでのフロー解説
- http://localhost:3000/login にアクセスするとReactは public/index.html ファイルを読み込みます
- src/index.js ファイルはアプリケーションのエントリポイントで、ReactDOM.createRoot を使って Appコンポーネントを #root 要素にレンダリングします
- 次にApp コンポーネントが実行されます。このコンポーネント内で、ルーティングが設定されている場合、React Routerが特定のパス（この場合は /login）に基づいて対応するコンポーネント（LoginForm）をレンダリングします。
- LoginForm コンポーネントがレンダリングされ、ログインフォームがユーザーに表示されます。ユーザーはフォームに入力し、ログイン情報を送信できます
- LoginForm の handleSubmit 関数が、フォームの送信時に実行されます。この関数は、ユーザーの入力をDjangoサーバーの /api/login/ エンドポイントにPOSTリクエストとして送信し、応答に基づいて適切なアクションを行います


以上でReactとDjangoを使用したログイン機能を実装できました。

PS：LoginFormの見た目を良くするため、CSSを作成していたので、CSSも含めたい場合は
LoginForm.jsと同じディレクトリにLoginForm.cssを作成してください。

```css:LoginForm.css
/* LoginForm.css */
.login-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh; /* ビューポートの高さに合わせる */
}

.login-form {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 10px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.login-input {
    margin-bottom: 10px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
    width: 200px;
}

.login-button {
    background-color: #007bff;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    width: 220px; /* インプットと同じ幅に */
}

.login-button:hover {
    background-color: #0056b3;
}

```