【DRF×Next】環境構築 ~ Googleログインまで



# はじめに
今回は、バックエンドをDjangoRestFrameworkで実装し、
フロントはNext（TypeScript）で**react-oauth**というサードパーティライブラリを使用して簡単にWebアプリ開発の土台を作成していきます。

# 環境
Ububtu 20.04

# 構成概要

```
PROJ #作成するプロジェクト
│── back # Django REST framework(以下DRF)のAPI
│   ├── api # モデル、ビュー、APIのルーティングなど
│   ├── env # venv
│   ├── manage.py # DRFのサーバ起動やマイグレーション
│   ├── project # DRFの設定もろもろの中核
│   └── requirements.txt # 必要なPythonパッケージ
│── front # フロントエンド
     └── client # Reactアプリケーション
```

# データベース

※SQLiteを使用する場合は無視してください。
DRFはデフォルトでSQLiteです。

`(sudo apt update && apt upgrade)`

`sudo apt install mysql-server`

**rootユーザのパスワード設定**

`sudo mysql_secure_installation`

`mysql -uroot -p`

mysql > `create database 任意のデータベース名;`

mysql > `quit`

# バックエンド

Django REST framework(DRF)を使用します。
Djangoとは別物と考えてください。

`sudo apt install python3-django`

### ①**必要ライブラリのインストール**

`python -m venv env`

(「env」は任意)

`source env/bin/activate`

```requirements.txt
Django
djangorestframework
django-cors-headers
drf_social_oauth2
mysqlclient
```

※django-cors-headersはポートの異なるフロントエンドからの通信を許可するのに必要です。

`pip install -r requirements.txt`

後から追加ライブラリがあればrequirements.txtを更新します。

`pip freeze > requirements.txt`

### ②D**jangoプロジェクトを作成**

`django-admin startproject project .`

（project名「project」は任意）

### ③**appを作成**

`python3 [manage.py](http://manage.py/) startapp api`

(app名「api」は任意)

### ④**設定ファイルを修正**

- 使用するライブラリと上で作成したapp名を登録
- db設定（dbエンジン, host, user, password）
- 言語、地域設定（日本・アジア・東京）

以下にスニペットを示します。

```project/settings.py
INSTALLED_APPS = [
   'app',
   'rest_framework',
   'corsheaders',
]
MIDDLEWARE = [
   'corsheaders.middleware.CorsMiddleware'
]
DATABASES = {
    #デフォのSQLite設定はコメントアウトまたは削除
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 上で作成した任意のDB名,
        'USER': 'root',
        'PASSWORD': 上で設定したrootのパスワード,
    }
}
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
CORS_ALLOWED_ORIGINS = [
   "http://localhost:3000",
]
```

### ⑤**管理者を作成**

`python3 manage.py createsuperuser`

上で設定したdbのauth_userテーブルに作成されます。

※以下api直下での作業です。

### ⑥**モデルを定義**

models.pyに作成するテーブルを記述します。

以下、例を示します。

```models.py
from django.db import models

# Create your models here.
class User(models.Model):
    email = models.EmailField(
        verbose_name='Eメールアドレス',
        max_length=255,
        unique=True,
    )
    name = models.CharField(max_length=100)
    family_name = models.CharField(max_length=100)
    given_name = models.CharField(max_length=100)
    picture = models.CharField(max_length=200)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name # 管理画面での一覧に表示する値。なくてもいいし別の値でもいい。　
```

### ⑦**マイグレート**

`python3 manage.py makemigrations api`

`python3 manage.py migrate`

appのモデルにテーブルを新しく定義する場合は再度上記２コマンド実行してください。

### ⑧**管理画面で操作できるようにする**

管理したいモデルを追加します。

以下、Userテーブルを追加する例です。

```admin.py
from django.contrib import admin
from .models import User

# ここに使用するモデルを設定してください
admin.site.register(User)
```

### ⑨**リクエストを受けてCRUD操作をするviewsを作成**

**serializer.py**

まずシリアライザを設定します。

Djangoにはない機能。DRF独自なのでファイルは新規作成してください。

シリアライザは複雑なデータをDBレコード⇔モデル間で橋渡ししてくれます。

以下、例です。

```serializer.py
from .models import User
from rest_framework import serializers
# from django.db.models import fields

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
　      fields = ('id','email','name','family_name','given_name','picture','active','created_at','updated_at')
```

**views.py**

以下、クラスベースのビューを採用する場合の例です。（関数ベースでも定義可能）

DRF公式によると以下のメリットがある。

・ 特定の HTTP メソッド (GET、POSTなど) に関連するコードの集まりを、条件分岐を使ってかき分けるのではなく、それぞれに独立したメソッドを割り当てることができる。

・ ミックスイン (多重継承) などのオブジェクト指向のテクニックを使って、コードを再利用可能なコンポーネントに分解できる。

```views.py
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404

from .models import User
from .serializer import UserSerializer
 
class UserListCreateAPIView(APIView): # 全データ取得とデータ追加についてのクラス
    def get(self, request): # 全データ取得
        articles = User.objects.filter(active=True)
        serializer = UserSerializer(articles, many=True)
        return Response(serializer.data)
 
    def post(self, request): # データ追加
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class UserDetailAPIView(APIView): # idをもとに個別データ取得
    err_msg = {
        "error": {
            "code": 404,
            "message": "Article not found",
        }}

    def get_object(self, pk):
        user = get_object_or_404(User, pk=pk)
        return user

    def get(self, request, pk):
        user = self.get_object(pk)
        serializer = UserSerializer(user)
        return Response(serializer.data)

    def put(self, request, pk):
        user = self.get_object(pk)
        serializer = UserSerializer(user, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        user = self.get_object(pk)
        user.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
```

### ⑩APIのエンドポイントと管理画面の**ルーティング**

```mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('app.urls')) # APIのバージョンを表記するのがREST界隈での慣習らしいです。
]
```

```api/urls.py
from django.urls import path
from .views import (
    UserListCreateAPIView,
    UserDetailAPIView
)

urlpatterns = [
    path("user/", UserListCreateAPIView.as_view(), name="article-list"),
    path("user/<int:pk>", UserDetailAPIView.as_view(), name="article-detail"),
]
```

### ⑪起動

`python3 [manage.py](http://manage.py) runserver`

でサーバを起動して、

`localhost:8000/admin`

の管理画面でテーブルを直接変更したり、

`localhost:8000/api/v1/user`

`localhost:8000/api/v1/user/(ユーザid)`

でhttp通信をしてみてください。

（get, post, put, deleteメソッドすべてやってみてください。）

あとでReact側からaxiosというhttp通信ライブラリでhttpリクエストをして、
レスポンスが帰ってくる感覚が簡単に実感できるかと思います。

---

# フロントエンド

※googleアカウントで認証してDRFにpostメソッドでユーザ追加できるようにするところまでの環境構築方法

**筆者のnode環境**

`node -v`

v18.12.1

`npm -v`

8.19.2

`npx -v`

8.19.2

### ①アプリ作成

`npx create-react-app client`

or

`npx create-next-app client`

next.jsを使用すればルーティングがpages直下のフォルダ構成と連動するので楽です。

しかし今回はまだnext.jsの特徴であるバックエンド機能は使用しません。

**create-appがうまく行かなければキャッシュクリア＆.npm削除してみてください。**

`npm cache clean --force`

`rm -rf ~/.npm`

### ②googleAOuth認証⇒DRFにリクエスト に必要なnpmパッケージをインストール

`npm i @react-oauth/google@latest axios jwt-decode`

- **react-oauth**
ReactでGoogleOAuth認証するためのライブラリ。
react-google-loginというライブラリもあるが、非推奨。
- **axios**
javascriptでのhttp通信を簡単に実装できるライブラリ。
Promissベース（処理が完了するまで次の処理に移行しない。）
- **jwt-decode**
react-oauthで認証成功した際のレスポンスにcredentialというキーがある。
JWTという符号化やデジタル署名の仕組みを規定した標準規格であるためデコードする必要がある。
デコードするとユーザ名、メアド、プロフィール写真のURLなどを取得可能。

### ③OAuthクライアントIDの取得

以下の記事を見れば簡単に行うことができます。

*[ReactとDjangoでGoogle OAuthログイン【前編】Google Developer ConsoleからクライアントIDとクライアントシークレットを取得する](https://qiita.com/baby-degu/items/1f011e97b98f9b0fcf01)*

### ④画面実装

以下、Next.js + TypeScriptで実装する場合の例です。

```pages/login.tsx
import { GoogleOAuthProvider } from '@react-oauth/google';
import { GoogleLogin } from '@react-oauth/google';
import axios from "axios";
import jwt_decode from 'jwt-decode'

const googleClientId: string = クライアントID;
const baseURL: string = "localhost:8000/api/v1";

const handleGoogleLogin = (response: any) => {
  console.log("response",response)
  var decoded: any = jwt_decode(response.credential)
  console.log("decoded",decoded)

  const body = {
    "email": decoded.email,
    "name": decoded.name,
    "family_name": decoded.family_name,
    "given_name": decoded.given_name,
    "picture": decoded.picture
  }

  axios.post(`${baseURL}/user/`, body)
  .then((res) => {
      console.log("Success Google login", res)
      window.location.href = "/"
  })
  .catch((err) => {
      console.log("Error Google login", err);
  });
};

export default function Login() {
  return (
      <div>
        <GoogleOAuthProvider clientId={googleClientId}>
          <GoogleLogin
            onSuccess={(response) => handleGoogleLogin(response)}
            onError={() => {
              console.log('Login Failed');
            }}
          />
        </GoogleOAuthProvider>
      </div>
  )
}
```

### ⑤起動＆GoogleOAuth認証

`npm run dev`

認証してDBを確認してください。

api_userテーブルにユーザが追加されていればOKです。

# 参考
https://zenn.dev/someone7140/articles/5a92eb3ca803f1

https://github.com/MomenSherif/react-oauth

https://qiita.com/baby-degu/items/1f011e97b98f9b0fcf01

https://qiita.com/baby-degu/items/51ab5c14ed6af23190e3