DRF カスタムユーザー

## はじめに

`dj_rest_auth`を導入したDRFのプロジェクトにカスタムユーザーの設定を行います。  

### 環境

- Intel Mac 13.4.1(c)
- Python 3.11.4
- Django 4.2.3
- DRF 3.14.0
- dj-rest-auth 4.0.1

### やりたいこと

- ユーザーのIDをUUIDにする。
- メールアドレス+パスワードでログインでする。
- ユーザーの属性は`first_name`, `last_name`ではなく`nick_name`にする。
- ログイン画面のリクエスト項目から`username`を除去する
- ユーザー情報のレスポンス項目を変更する
  - `pk`属性の名前を`id`にする
  - 返却するのは`id`, `email`, `nick_name`

## カスタムユーザー

### アカウント用プロジェクトの作成

アカウント用のプロジェクトを`accounts`という名前で作成して、`settings.py`に登録します。

```sh
$ ./manage.py startapp accounts 
```

```diff:config/settings.py
  INSTALLED_APPS = [
      ---省略---
+     "accounts.apps.AccountsConfig",
  ]
```



### カスタムユーザー/マネージャーの作成

`django.contrib.auth.base_user.AbstractBaseUser`を継承したカスタムユーザーのクラスを作成し、必要なフィールド情報を設定します。

```py:accounts/models.py
import uuid
from django.db import models
from django.contrib.auth.models import (
    BaseUserManager,
    AbstractBaseUser,
    PermissionsMixin,
)


class UserManager(BaseUserManager):
    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError("The given email must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

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

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        return self._create_user(email=email, password=password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    class Meta:
        db_table = "user"

    # 必要な属性を指定
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    email = models.EmailField("メールアドレス", unique=True)
    nick_name = models.CharField("ニックネーム", max_length=255, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)

    objects = UserManager()

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "email"

    # `createsuperuser`コマンドで入力するフィールドを追加
    REQUIRED_FIELDS = ["nick_name"]

    def __str__(self):
        return self.email
```

### カスタムユーザーを利用するように設定

`settings.py`にカスタムユーザーを利用するための設定を追加します。

```diff:config/settings.py
+ # カスタムユーザー
+ AUTH_USER_MODEL = "accounts.User"
```

### マイグレーション

マイグレーションしてDBに適用します。

```sh
$ ./manage.py makemigrations accounts
$ ./manage.py migrate
```

作成されたテーブルを確認すると、以下のようになっていました。(SQLite)

```sh
$ ./manage.py dbshell        
SQLite version 3.32.2 2021-07-12 15:00:17
sqlite> .header on
sqlite> .mode column
sqlite> pragma table_info('user');
cid         name        type          notnull     dflt_value  pk        
----------  ----------  ------------  ----------  ----------  ----------
0           password    varchar(128)  1                       0         
1           last_login  datetime      0                       0         
2           is_superus  bool          1                       0         
3           id          char(32)      1                       1         
4           email       varchar(254)  1                       0         
5           nick_name   varchar(255)  1                       0         
6           is_staff    bool          1                       0         
7           is_active   bool          1                       0    
```

### 動作確認

スーパーユーザーを作成してみます。

```sh
$ ./manage.py createsuperuser
メールアドレス: admin@example.com
ニックネーム: AdminUser
Password: 
Password (again): 
Superuser created successfully.
```

期待通り「ニックネーム」の入力ができました。また、管理サイトのログイン画面も

![管理サイトログイン画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3519083/318e18dd-214e-5a4c-8ecf-1ccdcfb2b499.png)

上記のように「メールアドレス」「パスワード」での入力に変わりました。



## dj_authのメッセージ構造を調整

`dj_auth`がログイン周りで提供してくれているメッセージ構造を少しだけ変更します。

https://dj-rest-auth.readthedocs.io/en/latest/configuration.html

### POST /login/

リクエストボディーから`username`を除去して`email`を必須にします。

![ログイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3519083/b0c60673-7635-bedf-3c4d-4e7bf528fcc2.png)


### GET /user/

レスポンスボディーの内容を変更します。

![ユーザー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3519083/37689019-5d9d-0e42-7a3a-42f8c22f5b34.png)

### 変更内容

`dj_rest_auth.serializers.LoginSerializer`を継承して修正します。

```python:accounts/serializers.py
from dj_rest_auth.serializers import LoginSerializer, UserDetailsSerializer
from django.contrib.auth import authenticate, get_user_model
from rest_framework import serializers

UserModel = get_user_model()


class LoginSerializer(LoginSerializer):
    username = None
    email = serializers.EmailField(required=True, allow_blank=False)


class UserDetailsSerializer(UserDetailsSerializer):
    class Meta:
        model = UserModel
        fields = ("id", "email", "nick_name")
        read_only_fields = ("email",)
```

作成したシリアライザーを利用するように`settings.py`で指定します。


```diff:config/settings.py
  # dj-rest-auth
  REST_AUTH = {
      "USE_JWT": True,
+     "LOGIN_SERIALIZER": "accounts.serializers.LoginSerializer",
+     "USER_DETAILS_SERIALIZER": "accounts.serializers.UserDetailsSerializer",
  }
```