Django Ninjaを使ってみた Vol.2 (User認証編)

### 環境
MacOS Ventura
Python 3.10.7
Poetry 1.3.2

## はじめに
この記事はこちらの続きです
https://qiita.com/ps0317ix/items/344471dd89a10ab520cd

## 早速始めよう
まずは、user用のディレクトリを作っていきます
```python:terminal
python manage.py startapp user
```

models.pyを以下の内容に変更します。
今回はDjango-ninjaがメインなので、Djangoのモデルなどに関する詳しい説明は割愛
```python:user/models.py
from django.db import models
from django.contrib.auth.models import PermissionsMixin, Group, Permission
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        if not email:
            raise ValueError('Emailを入力して下さい')
        email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self.db)
        return user

    def create_user(self, username, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_staff') is not True:
            raise ValueError('is_staff=Trueである必要があります。')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('is_superuser=Trueである必要があります。')
        return self._create_user(username, email, password, **extra_fields)

    def update_or_create_user(self, username=None, email=None, password=None, username_new=None, **extra_fields):
        try:
            user = self.get(email=email)
            updated = False
            if username_new:
                user.username = username_new
                updated = True
            for key, value in extra_fields.items():
                if getattr(user, key) != value:
                    setattr(user, key, value)
                    updated = True
            if updated:
                user.save(using=self.db)
        except self.model.DoesNotExist:
            user = self.create_user(username, email, password, **extra_fields)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(_("username"), max_length=50, blank=True)
    email = models.EmailField(_("email"), unique=True)
    avatar = models.ImageField(_("avatar"), upload_to='avatars', blank=True)
    avatar_url = models.URLField(_("avatar_url"), max_length=1500, blank=True)
    router = models.JSONField(_("router"), blank=True, null=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    groups = models.ManyToManyField(Group, verbose_name=_("groups"), blank=True, related_name="users")
    user_permissions = models.ManyToManyField(Permission, verbose_name=_("user permissions"), blank=True, related_name="users")
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True)

    objects = UserManager()
    USERNAME_FIELD = "email"
    EMAIL_FIELD = "email"
    REQUIRED_FIELDS = ['username']

    class Meta:
        db_table = 'user_user'
        verbose_name = "user"
        verbose_name_plural = "users"

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def email_user(self, subject, message, from_email=None, **kwargs):
        send_mail(subject, message, from_email, [self.email], **kwargs)

```

続いてスキーマを定義します。
```python:user/schema.py
from pydantic.types import SecretStr
from ninja import Schema
from ninja import ModelSchema
from django.contrib.auth import get_user_model


class UserSchema(ModelSchema):
    class Config:
        model = get_user_model()
        model_fields = ['id', 'username', 'email', 'is_staff', 'is_active', 'is_superuser']


class UseLogin(Schema):
    username: str
    email: str
    password: SecretStr


class UserIn(Schema):
    id: str
    username: str
    email: str
    openai_api_key: str


class UserOut(Schema):
    id: int
    username: str
    email: str
    roles: list[str] = ['client']
```

上記ができたらapiを作成していきます
```python:user/api.py
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model

from ninja_extra import (
    http_get, http_post, http_generic, http_put, http_delete,
    api_controller, status, ControllerBase, pagination
)
from ninja_extra.controllers.response import Detail
from ninja_jwt.authentication import JWTAuth

from .schema import UserSchema, UseLogin, UserIn, UserOut,

@api_controller('user', tags=['User'], auth=[JWTAuth()])
class UserController(ControllerBase):
    user_model = get_user_model()

    @http_get("/me", response=UserOut)
    def get_user(self, request):
        roles = []
        if request.user.is_superuser:
            roles.append('admin')
        elif request.user.is_staff:
            roles.append('staff')
        else:
            roles.append('client')

        if len(request.user.avatar_url) > 0:
            avatar = request.user.avatar_url
        else:
            avatar = request.user.avatar
        return {
            'id': request.user.id,
            'username': request.user.username,
            'email': request.user.email,
            'avatar': avatar,
            'roles': roles
        }

    @http_post('/create', response=UserSchema)
    def create_user(self, payload: UseLogin):
        return User.objects.create_user(**payload.dict())

    @http_put('/update', response=UserSchema)
    def update_user(self, request, payload: UserIn):
        user = self.user_model.objects.update_or_create_user(
            username=request.user.username,
            email=request.user.email
        )
        return user

    @http_delete('/{int:user_id}', response=Detail(status_code=status.HTTP_204_NO_CONTENT))
    def delete_user(self, user_id: int):
        user = self.get_object_or_exception(self.user_model, id=user_id)
        user.delete()
        return self.create_response('', status_code=status.HTTP_204_NO_CONTENT)
```
DJANGO_NINJA_TEMPLETEディレクトリに戻り、上記のController, appsを登録
```python:DJANGO_NINJA_TEMPLETE/api.py
from ninja_jwt.controller import NinjaJWTDefaultController
from ninja_extra import NinjaExtraAPI

from user.api import UserController # 追加

api = NinjaExtraAPI()
api.register_controllers(NinjaJWTDefaultController)
api.register_controllers(UserController) # 追加
```
```python:DJANGO_NINJA_TEMPLETE/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user', # 追加
]
```

migrationファイルを生成
```python:terminal
python manage.py makemigrations
# ImageFieldを入れる場合、pip install Pillowしてください
```
そして、migrateを生成
```python:terminal
python manage.py migrate
```
http://127.0.0.1:8000/api/docs
に遷移し、鍵マーク付きのエンドポイントが生成されていたら成功
![スクリーンショット 2023-03-13 19.13.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046008/0d075c1d-0cf4-b6f2-9650-c779ed40e9de.png)

## 確認
以下のコマンドでsuperuserを作成
```python:terminal
python manage.py createsuperuser
```
/api/token/pairのエンドポイントで、登録したユーザー情報を入力し、tokenが生成されたら成功！！
![スクリーンショット 2023-03-13 19.17.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046008/ce5dc342-4ea0-242a-028a-66c7f2c1e56c.png)

## 最後に
圧倒的に少ないコード量で実装できて、速度もDjangoより早くて、docsなども自動生成されて、革命的に感じるのは私だけですかね。

こういったサービス運営やってるので、気になる方はぜひ
https://data-lab.project-g.co.jp?utm_source=Qiita&utm_medium=qiita&utm_campaign=qiita_20230３１３

参考

https://eadwincode.github.io/django-ninja-jwt/settings/



※2023/05/17追記
デプロイ編を公開しました
https://qiita.com/ps0317ix/items/07af7a863ff63ad3dc81