【Django】仮登録ユーザーをEmail認証後にアクティベーションする実装ナレッジ

# はじめに

巷に出回っているアプリなどではユーザー登録されるときに、Emailによるアクティベーションを要求されることがあると思いますが、この記事ではDjangoでのその実装方法を記述していこうと思います

Djangoではデフォルトでユーザーモデルが定義されており、フィールドにis_activeというフィールドがデフォルトで定義されておりこちらを上手に使っていきます

# Djangoの設定

PythonとDjangoは導入できている前提とします

通常の手順通りアプリを作成していきます
```
django-admin startproject [プロジェクト名]
django-admin startapp [アプリ名]
```

アプリ名はbasicappとしておきます

settings.py
```python
INSTALLED_APPS = [
    'basicapp.apps.BasicappConfig',
]
```

プロジェクト側urls.pyの設定
/urls.py
```python
from django.conf.urls import include

urlpatterns = [
    path('', include('basicapp.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
]
```
accountsのurlはDjangoにデフォルトで組み込まれているログイン認証機能を使用します
用意されているパターンはこちら
```
accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']
```



アプリ側urls.pyの設定

basicapp/urls.py
```python
from rest_framework.routers import DefaultRouter
from django.urls import path
from django.conf.urls import include

router = DefaultRouter()

app_name = 'basicapi'
urlpatterns = [
    path('', include(router.urls)),

]
```

# Email機能の設定

以下を記載するとコンソールにEmailを表示させることができる
これだけでEmail機能を簡単にデモすることができる

settings.py
```
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # コンソールにメール内容を表示する
EMAIL_HOST = ホスト情報
EMAIL_PORT = ポート情報
DEFAULT_FROM_EMAIL = デフォルト送信元
EMAIL_HOST_USER = ユーザー
EMAIL_HOST_PASSWORD = パスワード
EMAIL_USE_TLS = TLSを使うかどうか

例　
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = 'hoge@hoge.com'
EMAIL_HOST_USER = 'hoge@hoge.com'
EMAIL_HOST_PASSWORD = 'password'
EMAIL_USE_TLS = True
```

なおメールを実際に飛ばす場合は以下の設定になる
settings.py
```
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # メールを送信する
```


# カスタムユーザー定義

## カスタムユーザー適用準備
次に、今後Djangoで扱うユーザーはカスタムユーザーを使用していくので、ユーザーの設定を記載します

settings.py
```python
AUTH_USER_MODEL = 'basicapp.User'
```

## カスタムユーザーの実装

カスタムユーザーを使用するためのマネージャークラスとカスタムユーザーのモデルを作ります
Djangoのデフォルトではユーザー名がログインに使用されますが、emailをユーザー名として利用するようにします(USERNAME_FIELD の部分で変更可能)
アカウント生成時はユーザー仮登録としたいのでis_activeをFalseにします

models.py
```python
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser,  PermissionsMixin
import uuid

class UserManager(BaseUserManager):

    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(email=self.normalize_email(email), **extra_fields)
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)

        return user


class User(AbstractBaseUser, PermissionsMixin):

    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    email = models.EmailField(max_length=255, unique=True)
    username = models.CharField(max_length=255, blank=True)
    is_active = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'

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

## 有効期限付きアクティベーション用トークン保持テーブルを実装

Email認証には特定のトークンをくっつけたURLをクリックすることでアクティベーションを行える実装としたいので、アクティベーショントークンを保持するテーブルを作ります

models.py
```python
class UserActivateTokens(models.Model):

    token_id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    activate_token = models.UUIDField(default=uuid.uuid4)
    expired_at = models.DateTimeField()

    objects = UserActivateTokensManager()


class UserActivateTokensManager(models.Manager):

    def activate_user_by_token(self, activate_token):
        user_activate_token = self.filter(
            activate_token=activate_token,
            expired_at__gte=datetime.now() # __gte = greater than equal
        ).first()
        if hasattr(user_activate_token, 'user'):
            user = user_activate_token.user
            user.is_active = True
            user.save()
            return user
```

## ユーザー生成時にトークンを発行しメール送信をする処理の実装

仮登録が完了すると同時にEmailが飛んでURLをクリックするとアクティベーションを行える処理を実装します
@receiverをつかったシグナリングの実装を行っています
今回ではsenderに設定したユーザーがPOSTメソッドで保存が実行されたときにアノテーション以下の処理が実行されます
Subjectは日本語で書くとなぜか文字化けします

```python
from django.conf import settings
from datetime import datetime, timedelta
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.mail import send_mail


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def publish_activate_token(sender, instance, **kwargs):
    if not instance.is_active:
        user_activate_token = UserActivateTokens.objects.create(
            user=instance,
            expired_at=datetime.now()+timedelta(days=settings.ACTIVATION_EXPIRED_DAYS),
        )
        subject = 'Please Activate Your Account'
        message = f'URLにアクセスしてユーザーアクティベーション。\n http://127.0.0.1:8000/users/{user_activate_token.activate_token}/activation/'
    if instance.is_active:
        subject = 'Activated! Your Account!'
        message = 'ユーザーが使用できるようになりました'
    from_email = settings.DEFAULT_FROM_EMAIL
    recipient_list = [
        instance.email,
    ]
    send_mail(subject, message, from_email, recipient_list)
```

トークンの有効期限もsettingに設定します
3日間有効としています
settings.py
```python
ACTIVATION_EXPIRED_DAYS = 3
```


# View.pyの実装

アクティベーション用のURLが叩かれたらHTTPResponseを返す単純なViewを作ります
views.py
```python
from .models import UserActivateTokens
from django.http import HttpResponse


def activate_user(request, activate_token):
    activated_user = UserActivateTokens.objects.activate_user_by_token(
        activate_token)
    if hasattr(activated_user, 'is_active'):
        if activated_user.is_active:
            message = 'ユーザーのアクティベーションが完了しました'
        if not activated_user.is_active:
            message = 'アクティベーションが失敗しています。管理者に問い合わせてください'
    if not hasattr(activated_user, 'is_active'):
        message = 'エラーが発生しました'
    return HttpResponse(message)
```

ulrsに追記

urls.py
```python
from .views import activate_user

urlpatterns = [
    path('users/<uuid:activate_token>/activation/', activate_user, name='users-activation'),
]
```

# Admin画面の設定

adminpy
```
from django.contrib import admin
from .models import User, UserActivateTokens
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin


class UserAdmin(BaseUserAdmin):
    ordering = ('id',)
    list_display = ('email', 'id', 'is_active', 'password')
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal Information', {'fields': ('username',)}),
        (
            'Permissions',
            {
                'fields': (
                    'is_active',
                    'is_staff',
                    'is_superuser',
                )
            }
        ),
        ('Important dates', {'fields': ('last_login',)}),
    )
    add_fieldsets = (
        (None, {
           'classes': ('wide',),
           'fields': ('email', 'password1', 'password2'),
        }),
    )


class UserActivateTokensAdmin(admin.ModelAdmin):
    list_display = ('token_id', 'user', 'activate_token', 'expired_at')



admin.site.register(User, UserAdmin)
admin.site.register(UserActivateTokens, UserActivateTokensAdmin)
```


# 動作確認

それではマイグレーションをまずはします

```
py manage.py makemigrations
py manage.py migrate
py manage.py runserver
py manage.py createsuperuser
```

その後、Superユーザー作成をした時にコンソールにEmailの内容が表示されるはずなので、URLをクリックしてアクティベーションされるか確認しましょう
URLをクリックする前はAdmin画面にログインできなかったと思いますが、クリック後は出来るようになっていればOKです

また、管理者画面でユーザーを追加しても同様にメールが飛ぶはずなので、URLをクリックしてis_actioveになるかを確認しましょう

```
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: Please Activate Your Account
From: hoge@hoge.com
To: user1@user.com
Date: 
Message-ID: <>

URLにアクセスしてユーザーアクティベーション。
 http://127.0.0.1:8000/users/................/activation/
-------------------------------------------------------------------------------
```

確認出来たら成功です




# より実用的なアプリにおける実装方法

より実用的なアプリにおけるEmailアクティベーションの実装方法を知りたい方はこちらの記事をご覧ください
マッチングを一から作りながら諸々の実装を解説しています

https://happy-daikicchi.com/archives/28