【Next.js×Django連携】管理者ログイン画面を作成する方法

# サンプル
このような管理者ログイン画面を作っていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/6828fa25-228a-421b-964f-286bd26cf220.png)


# フロントエンド側(Next.js)の実装

::: note info
#### ちょっとしたchip
POST リクエストを送信しているURLが APPEND_SLASH が有効な状態で、URLの末尾にスラッシュ`（/）`が不足しないように、スラッシュ`（/）`を付与しておく。

```
const response = await axios.post('http://127.0.0.1:8000/api/loginAdminUsers/',formData);
```

#### Djangoの設定でAPPEND_SLASHを無効にする
Djangoの設定で、APPEND_SLASHを False に設定することでも、末尾のスラッシュを強制しないようにできます。この設定を有効にすることで、URLの末尾にスラッシュを追加しなくてもDjangoはリダイレクトしません。

settings.py 内の APPEND_SLASH を False に変更します。
```
# settings.py
APPEND_SLASH = False
```

:::


```frontend/app/adminUsers/login/page.tsx
'use client'
import { useState } from "react"
import { useForm } from "react-hook-form"
import axios from "axios"
import { useRouter } from "next/navigation"

export default function AdminLogin(){
    const router = useRouter();
    const defaultValues = {
        adminUserName:'',
        adminUserEmail:'',
        adminUserPassword:''
    };

    const {register,handleSubmit,formState:{errors}} = useForm({
        defaultValues
    })

    const [adminUserName,setAdminUserName] = useState('');
    const [adminUserEmail,setAdminUserEmail] = useState('');
    const [adminUserPassword,setAdminUserPassword] = useState('');

    const onSubmit = async(data:any)=>{
        try{
            const formData = new FormData();
            formData.append('adminUserName',data.adminUserName);
            formData.append('adminUserEmail',data.adminUserEmail);
            formData.append('adminUserPassword',data.adminUserPassword);
    
            const response = await axios.post('http://127.0.0.1:8000/api/loginAdminUsers/',formData);
    
            if(response.status === 201){
                alert('Success to Login');
                console.log(response.data);
            }else{
                alert('Error');
                console.log(response.data);
            }
        }catch(error){
            console.log(error);
        }

    }

    const clickBackToTopPage = ()=>{
        router.push('/');
    }

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label>Name</label>
                <input
                    type="text"
                    className="w-full border border-gray-500 rouden-md"
                    defaultValue={defaultValues.adminUserName}
                    {...register('adminUserName',{
                        required:'AdminUserName must be required.'
                    })}
                    onChange={(e)=>setAdminUserName(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserName?.message}</div>
            </div>
            <div>
                <label>Email</label>
                <input
                    type="email"
                    className="w-full border border-gray-500 rouden-md"
                    defaultValue={defaultValues.adminUserEmail}
                    {...register('adminUserEmail',{
                        required:'Email must be required.'
                    })}
                    onChange={(e)=>setAdminUserEmail(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserEmail?.message}</div>
            </div>
            <div>
                <label>Password</label>
                <input
                    type="password"
                    className="w-full border border-gray-500 rouden-md"
                    defaultValue={defaultValues.adminUserPassword}
                    {...register('adminUserPassword',{
                        required:'Password must be required.'
                    })}
                    onChange={(e)=>setAdminUserPassword(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserPassword?.message}</div>
            </div>
            <div>
                <button
                    type="submit"
                    className="bg bg-blue-500 rouden-md text-white px-4 py-4"
                >
                    Login
                </button>
                <button
                    type="button"
                    className="bg bg-green-500 rouden-md text-white px-4 py-4"
                    onClick={clickBackToTopPage}
                >
                    Back To TopPage
                </button>
            </div>
        </form>
    )
}

```

# サーバ側(Django)の実装

ルーティング設定は、下記の通り。
今回はログイン機能ですので、`login_admin_user`関数をルートに含めます。
なので、まずは`login_admin_user`関数をインポートします。


```backend/api/url.py
from .views import TodoViewSet, AdminUsersViewSet, login_admin_user
```

つぎに`login_admin_user`関数をルートに含めます。これにより、POST リクエストで `/loginAdminUsers/` エンドポイントにアクセスできるようになります。

```backend/api/url.py
urlpatterns = [
    path('loginAdminUsers/', login_admin_user, name='login_admin_user'),  # ログイン用のエンドポイント
    path('', include(router.urls)),
]
```

全体のコードはこちら↓
```backend/api/url.py
from django.urls import path, include
from .views import TodoViewSet, AdminUsersViewSet, login_admin_user  # login_admin_user をインポート
from rest_framework.routers import DefaultRouter
from .views import TodoViewSet,AdminUsersViewSet

router = DefaultRouter()
router.register('todos', TodoViewSet, basename='todo')
router.register('adminusers',AdminUsersViewSet,basename='create_admin_user')

urlpatterns = [
    path('loginAdminUsers/', login_admin_user, name='login_admin_user'),  # ログイン用のエンドポイント
    path('', include(router.urls)),
]
```

## パスワードの照合

https://docs.djangoproject.com/ja/5.1/topics/auth/passwords/#:~:text=django.contrib.auth.hashers.PBKDF2PasswordHasher%22%2C

つづいて、`views.py`についてです。

パスワードの照合は、下記の処理を追加します。
```views.py
# パスワード照合
            if check_password(request.data['adminUserPassword'], loginAdminUser.adminUserPassword):  # ここで adminUserPassword を確認
                # シリアライザーを使ってユーザー情報を返す
                user_data = {
                    "id": loginAdminUser.id,
                    "adminUserName": loginAdminUser.adminUserName,
                    "adminUserEmail": loginAdminUser.adminUserEmail,
                    # 必要に応じて他の情報を追加
                }
                return Response(data=user_data, status=status.HTTP_200_OK)
            else:
                return Response(data={"message": "Invalid credentials."}, status=status.HTTP_401_UNAUTHORIZED)

```
全体のコードはこちら↓

```backend/api/views.py
#from django.shortcuts import render
from rest_framework.decorators import api_view #追加
from rest_framework.response import Response # 追加
from rest_framework import status #追加
from django.db import transaction # 追加
from django.contrib.auth.hashers import make_password #追加
from rest_framework import viewsets
from .models import Todo,AdminUsers
from .serializers import TodoSerializer,AdminUsersSerializer,AdminUserLoginSerializer
from django.core.exceptions import ObjectDoesNotExist #Add
from django.contrib.auth.hashers import check_password #Add


# Create your views here.
class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

# Serialize to create adminuser
class AdminUsersViewSet(viewsets.ModelViewSet):
    queryset = AdminUsers.objects.all()
    serializer_class = AdminUsersSerializer

@api_view(['POST'])
def create_admin_user(request):
    if request.method == 'POST':
        # Start transaction
        with transaction.atomic():
            serializer = AdminUsersSerializer(data=request.data)
            if serializer.is_valid():
                #Generate Password Hash
                password = serializer.validated_data['adminUserPassword']
                hashed_password = make_password(password)
                
                #Set hasedPassword to Data
                serializer.validated_data['adminUserPassword'] = hashed_password#hashed_password
                if serializer.is_valid():

                    serializer.save()
                    return Response(serializer.data,status=status.HTTP_201_CREATED)#serializer.data
            else:       
                return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)#serializer.data

# AdminLoginMethod
@api_view(['POST'])
def login_admin_user(request):
        if request.method == 'POST':
            print("Request Data:", request.data)  # デバッグ用にリクエストデータを表示

            # シリアライズしてバリデーション
            serializer = AdminUserLoginSerializer(data=request.data)

            # バリデーションエラーの場合
            if not serializer.is_valid():
                return Response(data={"message": "Invalid data provided."}, status=status.HTTP_400_BAD_REQUEST)
            
            try:
                # メールアドレスを使ってユーザーを一意に取得
                loginAdminUser = AdminUsers.objects.get(adminUserEmail=request.data['adminUserEmail'])
            except ObjectDoesNotExist:
                return Response(data={"message": "There is no admin information."}, status=status.HTTP_404_NOT_FOUND)

            # パスワード照合
            if check_password(request.data['adminUserPassword'], loginAdminUser.adminUserPassword):  # ここで adminUserPassword を確認
                # シリアライザーを使ってユーザー情報を返す
                user_data = {
                    "id": loginAdminUser.id,
                    "adminUserName": loginAdminUser.adminUserName,
                    "adminUserEmail": loginAdminUser.adminUserEmail,
                    # 必要に応じて他の情報を追加
                }
                return Response(data=user_data, status=status.HTTP_200_OK)
            else:
                return Response(data={"message": "Invalid credentials."}, status=status.HTTP_401_UNAUTHORIZED)

        
```

:::note info
#### 画面に返却するデータの制御
今回は①管理者名と②E-mailをレスポンスにしたいので、下記のようにする。
```serializers.py
     class Meta:
        model = AdminUsers
        fields = ['adminUserName','adminUserEmail', 'adminUserPassword']  
```

:::

さいごに`Serializer.py`についてです。
こちらは、管理者登録とは少し作りが変わります。

`AdminUserLoginSerializer`関数の引数は、`serializers.Serializer`となります。
引数を`serializers.ModelSerializer`にしてしまうと、serializerが`create`関数が呼び出してしまいデータベースに登録されてしまいます。

https://www.django-rest-framework.org/api-guide/serializers/


これでは困るので、引数を`serializers.Serializer`に設定します。


```backend/api/serializers.py
from rest_framework import serializers
from .models import Todo,AdminUsers
from django.contrib.auth.hashers import make_password

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ['id', 'title', 'completed', 'attachment']  # Include necessary fields

class AdminUsersSerializer(serializers.ModelSerializer):
    class Meta:
        model = AdminUsers
        fields = ['id','adminUserName','adminUserEmail','adminUserPhone','adminUserPassword','adminUserCompany','adminUserDepartment']
        adminUserPassword = serializers.CharField(write_only=True)

        extra_kwargs = {
            'adminUserPassword':{'write_only':True}
        }
   
    def create(self, validated_data):
        # パスワードをハッシュ化
        password = validated_data['adminUserPassword']
        hashed_password = make_password(password)
        validated_data['adminUserPassword'] = hashed_password
    
        # 新しいユーザーを作成
        return super().create(validated_data)

class AdminUserLoginSerializer(serializers.Serializer):
    adminUserEmail = serializers.EmailField()  # メールアドレスを受け取る
    adminUserPassword = serializers.CharField(write_only=True)  # パスワードを受け取る

    def validate_adminUserEmail(self, value):
        # メールアドレスのバリデーション
        if not value:
            raise serializers.ValidationError("Email must be provided.")
        return value
    
    def validate_adminUserPassword(self, value):
        # パスワードのバリデーション
        if not value:
            raise serializers.ValidationError("Password must be provided.")
        return value
    
    
```

# 参考サイト
#### Python | Django | React | Todo webアプリの作成方法

https://shelokuma.com/2024/10/03/how-to-todo-web-app-using-django-and-react/