DjangoMacBook PromacOSPythonReact メール認証ユーザ登録

メール認証方式のユーザ登録機能を実装する〜Reactjs + Django 環境|macOS〜

squirrel-mail-auth Django
スポンサーリンク

Reactjs + Django REST Framework ( DRF ) 環境を構築しながら、メール認証方法を適用したユーザ登録機能を実装します。Django ではデフォルトで username/email/password パターンでユーザを登録する仕組みなので、Email/password で登録 ( Log in / Sign up ) できるように Custom User Model を適用します。

現状・条件

  • バックエンド環境:Django / Django Rest Framework / Django-Rest-Auth
  • フロントエンド環境:React / Redux / Bootstrap
  • OS 環境 : MacBook Pro / macOS High Sierra
  • Python 3.7.2
  • PostgreSQL 11.3

バックエンド環境を構築

Django 環境を構築

email-auth ディレクトリを作成して作業を行います。( ディレクトリ名・場所は任意 )
バックエンドは最終的に下記 ( ▼ ) のような構造になります。

# ディレクトリのみ表示した場合 ( MacBook Pro )

(backend) try🐶everything backend$ tree -d
.
├── project   << Django プロジェクト名
├── tmp
│   └── emails   << ユーザ認証用メールを保存
├── user_profile   << Django アプリ名
│   └── migrations
└── users   << Django アプリ名
    └── migrations

7 directories
(backend) try🐶everything backend$ 
try🐶everything ~$ mkdir email-auth
try🐶everything ~$ cd email-auth/
try🐶everything email-auth$

別のツールを使用しても構いませんが、ここでは Python 仮想環境やパッケージを管理するため、pipenv を使用しますので、下記のコマンドでインストールしておきます。

try🐶everything email-auth$ brew install pipenv

バックエンド用ディレクトリ backend を生成します。

try🐶everything email-auth$ mkdir backend
try🐶everything email-auth$ cd backend/

そこに仮想環境を作成します。

コマンド:pipenv install

try🐶everything backend$ pipenv install
Creating a virtualenv for this project…
Pipfile: /Users/macadmin/email-auth/backend/Pipfile
Using /usr/local/bin/python3 (3.7.2) to create virtualenv…
⠦ Creating virtual environment...Using base prefix '/usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7'
New python executable in /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/python3.7
Also creating executable in /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/python
Installing setuptools, pip, wheel...
done.
Running virtualenv with interpreter /usr/local/bin/python3

✔ Successfully created virtual environment!
Virtualenv location: /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek
Installing dependencies from Pipfile.lock (03de17)…
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 15/15 — 00:00:16
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
try🐶everything backend$

仮想環境に入ります。

コマンド:pipenv shell ( 出る:exit )

try🐶everything backend$ pipenv shell
Launching subshell in virtual environment…
try🐶everything backend$  . /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/activate
(backend) try🐶everything backend$

仮想環境に必要なパッケージをインストールします。

2019/10/07 時点での、Python パッケージリストです。

ダウンロード後、pip install -r … コマンドでインストールします。

(backend) try🐶everything backend$ pip install -r ~/Downloads/requirements.txt

それでは、Django 環境を設定するため、新しいプロジェクトを生成し、settings.pyurls.py を編集します。

新しいプロジェクトを生成します。

プロジェクト名は任意ですが、ここでは project にします。

プロジェクトを生成後、project/settings.py を編集します。

(backend) try🐶everything backend$ django-admin startproject project .
(backend) try🐶everything backend$ cd project
(backend) try🐶everything project$ vi settings.py
project/settings.py
  • INSTALLED_APPS

    ユーザ認証や API 機能を使用するためのパッケージを有効化します。

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'django.contrib.sites',
    
        'rest_framework',
        'rest_framework.authtoken',
        'rest_auth',
        'rest_auth.registration',
    
        # Third-party
        'allauth',
        'allauth.account',
        'allauth.socialaccount',
    
        # 'allauth.socialaccount.providers.facebook',
        # 'allauth.socialaccount.providers.twitter',
    
        # rest cors support
        'corsheaders',
    
    
        # Local
        'users',
        'user_profile',
    ]
  • MIDDLEWARE
    MIDDLEWARE = [
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'django.middleware.security.SecurityMiddleware',
        'corsheaders.middleware.CorsMiddleware',
    ]
  • TEMPLATES
    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': ['templates'],
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
  • DATABASES:PostgreSQLを使用する場合
    # DATABASES = {
    #    'default': {
    #        'ENGINE': 'django.db.backends.sqlite3',
    #        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    #    }
    # }
    
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': 'データベース名',
            'USER': 'ユーザ名',
            'PASSWORD': 'パスワード',
            'HOST': 'localhost',
            'PORT': '',
        }
    }
  • TIME_ZONE / SITE_ID
    TIME_ZONE = 'Asia/Tokyo'
    
    SITE_ID = 1

  • AUTHENTICATION_BACKENDS

    ユーザー名の代わりに電子メールでの登録を可能にするために設定します。

    AUTHENTICATION_BACKENDS = (
        "django.contrib.auth.backends.ModelBackend",
        "allauth.account.auth_backends.AuthenticationBackend",
    )
  • django-allauth
    AUTH_USER_MODEL = 'users.CustomUser'
    ACCOUNT_ADAPTER = 'user_profile.adapter.MyAccountAdapter'
    ACCOUNT_USERNAME_REQUIRED = False
    ACCOUNT_EMAIL_REQUIRED = True
    ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
    ACCOUNT_UNIQUE_EMAIL = True
    ACCOUNT_USER_MODEL_EMAIL_FIELD = 'email'
  • django-allauth: account
    PASSWORD_RESET_TIMEOUT_DAYS = 1
    ACCOUNT_AUTHENTICATION_METHOD = 'email'
    ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3
    ACCOUNT_EMAIL_SUBJECT_PREFIX = ''
    ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
    ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False  # to keep the user logged in after password change
    

     

  • django-rest-auth:REST_AUTH_SERIALIZERS
    REST_AUTH_SERIALIZERS = {
        'USER_DETAILS_SERIALIZER': 'users.serializers.UserProfileSerializer'
    }
    REST_SESSION_LOGIN = False
    OLD_PASSWORD_FIELD_ENABLED = False
    LOGOUT_ON_PASSWORD_CHANGE = False
  • 10
    REST_FRAMEWORK
    REST_FRAMEWORK = {
        'DEFAULT_PERMISSION_CLASSES': (
            'rest_framework.permissions.IsAuthenticated',
            #'rest_framework.permissions.AllowAny',
        ),
        'DEFAULT_AUTHENTICATION_CLASSES': (
            'rest_framework.authentication.TokenAuthentication',
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.BasicAuthentication',
        ),
        'DEFAULT_THROTTLE_CLASSES': (
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle'
        ),
        'DEFAULT_THROTTLE_RATES': {
            'anon': '100/day',
            'user': '1000/day'
        },
        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 100,
    
        'DEFAULT_PARSER_CLASSES': (
            'rest_framework.parsers.JSONParser',
            'rest_framework.parsers.FormParser',
            'rest_framework.parsers.MultiPartParser'
         ),
    }
    
  • 11
    CORS Setting
    # Change CORS settings as needed
    # CORS_ORIGIN_ALLOW_ALL = True
    CORS_ORIGIN_ALLOW_ALL = False
    
    # https://github.com/ottoyiu/django-cors-headers
    # CORS_ORIGIN_WHITELIST = (
    #     'localhost:3000',
    #     '127.0.0.1:3000',
    #     '192.168.11.7:3000',
    # )
    
    # Or,
    
    CORS_ORIGIN_REGEX_WHITELIST = (
        r'^(http?://)?localhost',
        r'^(http?://)?127.',
        r'^(http?://)?192.168.11.',
    )
  • 12
    Email Auth Settings

    開発中には、tmp/emails 下に認証用メール本文が届きます。
    本番なら、[SMTP] Email Settings をご使用ください。

    #-------------------------
    # [Dev] Email Settings
    #-------------------------
    EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
    EMAIL_FILE_PATH = 'tmp/emails'
    DEFAULT_FROM_EMAIL = 'admin@example.com'
    
    #-------------------------
    # [SMTP] Email Settings
    #-------------------------
    #
    # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # default
    # DEFAULT_FROM_EMAIL = 'admin@example.com'
    # EMAIL_HOST = 'smtp.example.com'
    # EMAIL_HOST_USER = 'admin@example.com'
    # EMAIL_HOST_PASSWORD = 'your_password'
    # EMAIL_PORT = 465   # smtps
    # EMAIL_USE_SSL = True

次は、project/urls.py を編集します。

(backend) try🐶everything project$ vi urls.py
project/urls.py
  • 1
    router / urlpatterns
    from django.urls import re_path, include
    from django.contrib import admin
    from django.views.generic import TemplateView
    from rest_framework import routers
    from user_profile.views import UserViewSet
    
    router = routers.DefaultRouter()
    router.register(r'user', UserViewSet,) # ignored users.urls 'user' path
    
    urlpatterns = [
        re_path(r'^api/',  include(router.urls)),
        re_path(r'^api/',  include('users.urls')),
    
        # Authenticate API Site
        re_path(r'^api-auth/', include('rest_framework.urls')),
    
        # This is used for user reset password
        re_path(r'^', include('django.contrib.auth.urls')),
        re_path(r'^rest-auth/', include('rest_auth.urls')),
        re_path(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
        re_path(r'^account/', include('allauth.urls')),
        re_path(r'^admin/', admin.site.urls),
    ]
    

    この設定によって、DRF サイトのアクセス URL は localhost:8000 ではなく、localhost:8000/api/ から閲覧出来るようになります。

次は、ユーザー名の代わりに電子メールでの登録を可能にするための設定を行います。

Email / Password でログインできるように変更します。

※ 以下は djangoX でインサイトを得たものです。

Django アプリ users を作成します。( アプリ名は任意 )

(backend) try🐶everything backend$ django-admin startapp users
(backend) try🐶everything backend$ cd users

users/admin.py を編集します。

from django.contrib import admin
from .models import CustomUser

admin.site.register(CustomUser)

users/models.py を編集します。

from django.db import models
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils.translation import ugettext_lazy as _

class MyUserManager(BaseUserManager):
    """
    A custom user manager to work with emails instead of usernames
    """

    def create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The Email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, is_active=True, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_staff', True)
        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self.create_user(email, password, **extra_fields)

    def search(self, kwargs):
        qs = self.get_queryset()
        print(kwargs)
        if kwargs.get('first_name', ''):
            qs = qs.filter(first_name__icontains=kwargs['first_name'])
        if kwargs.get('last_name', ''):
            qs = qs.filter(last_name__icontains=kwargs['last_name'])
        if kwargs.get('department', ''):
            qs = qs.filter(department__name=kwargs['department'])
        if kwargs.get('company', ''):
            qs = qs.filter(company__name=kwargs['company'])
        return qs

class CustomUser(AbstractBaseUser, PermissionsMixin):
    """
    Customized User model itself
    """
    username = models.CharField(_('username'), max_length=150, blank=True)
    email = models.EmailField(unique=True, null=True)
    is_staff = models.BooleanField(default=False)
    is_active   = models.BooleanField(default=True)
    first_name = models.CharField(default='', max_length=60, blank=True)
    last_name = models.CharField(default='', max_length=60, blank=True)
    current_position = models.CharField(default='', max_length=64, blank=True)
    about = models.CharField(default='', max_length=255, blank=True)
    department = models.CharField(default='', max_length=128, blank=True)
    company = models.CharField(default='', max_length=128, blank=True)
    date_joined = models.DateTimeField(auto_now_add=True)
    USERNAME_FIELD = 'email'
    objects = MyUserManager()

    def __str__(self):
        return self.email

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

users/serializers.py を作成します。( 新 )

import hashlib
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from .models import CustomUser

class DynamicFieldsModelSerializer(serializers.ModelSerializer):

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)

class UserItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomUser
        fields = ('id', 'email', 'first_name', 'last_name', 'date_joined')

class UserProfileSerializer(serializers.ModelSerializer):
    """
    Class to serialize data for user profile details
    """

    class Meta:
        model = CustomUser
        fields = ('id', 'email', 'username', 'first_name', 'last_name',
                  'current_position', 'about', 'company', 'department', 'date_joined')


class UserSerializer(serializers.ModelSerializer):
    """
    Class to serialize data for user validation
    """
    first_name = serializers.CharField(max_length=60)
    last_name = serializers.CharField(max_length=60)
    current_position = serializers.CharField(max_length=64)
    about = serializers.CharField(max_length=255)

    class Meta:
        model = CustomUser
        fields = ('id', 'email','username', 'first_name', 'last_name',
                  'current_position', 'about', 'company', 'department','date_joined')

    def validate(self, data):
        if len(data['first_name']) + len(data['last_name']) > 60:
            raise serializers.ValidationError({
                'first_name': 'First + Last name should not exceed 60 chars'})
        return data


class UserRegistrationSerializer(serializers.Serializer):
    """
    Serializer for Registration - password match check
    """
    email = serializers.EmailField(required=True, validators=[
        UniqueValidator(queryset=CustomUser.objects.all())])
    password = serializers.CharField()
    confirm_password = serializers.CharField()

    def create(self, data):
        return CustomUser.objects.create_user(data['email'], data['password'])

    def validate(self, data):
        if not data.get('password') or not data.get('confirm_password'):
            raise serializers.ValidationError({
                'password': 'Please enter password and confirmation'})
        if data.get('password') != data.get('confirm_password'):
            raise serializers.ValidationError(
                {'password': 'Passwords don\'t match'})
        return data

users/urls.py を作成します。( 新 )

from django.urls import re_path
from users import views

urlpatterns = [
		re_path(r'^me/$', views.CurrentUser.as_view()),
		re_path(r'^register/$', views.Registration.as_view()),
		re_path(r'^users/$', views.UserList.as_view()),
		re_path(r'^user/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
		re_path(r'^search/$', views.UserSearch.as_view()),
]

users/views.py を編集します。

from django.http import Http404
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status, generics
from .serializers import (
    UserProfileSerializer,
    UserItemSerializer,
    UserRegistrationSerializer,
    UserSerializer,
)
from .models import CustomUser


def index(request):
    return render(request, 'index.html')


class UserList(APIView):
    """
    List all users
    """
    permission_classes = (IsAuthenticated, )

    def get(self, request, format=None):
        users = CustomUser.objects.all()
        serializer = UserItemSerializer(users, many=True)
        return Response({'users': serializer.data})


class UserSearch(APIView):
    """
    Advanced user search
    """
    permission_classes = (IsAuthenticated, )

    def post(self, request):
        if request.data:
            users = CustomUser.objects.search(request.data)
        else:
            users = CustomUser.objects.all()
        serializer = UserProfileSerializer(users, many=True)
        return Response({'users': serializer.data})


class UserDetail(APIView):
    """
    User profile details
    """
    permission_classes = (IsAuthenticated, )

    def get_object(self, pk):
        try:
            return CustomUser.objects.get(pk=pk)
        except Todo.DoesNotExist:
            raise Http404

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

    def put(self, request, pk, format=None):
        user = self.get_object(pk)
        if request.user != user:
            return Response(status=status.HTTP_403_FORBIDDEN)
        serializer = UserSerializer(user, request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({'success': True})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class CurrentUser(APIView):

   permission_classes = (IsAuthenticated, )

   def get(self, request, *args, **kwargs):
        if request.user.id == None:
            raise Http404
        serializer = UserProfileSerializer(request.user)
        data = serializer.data
        data['is_admin'] = request.user.is_superuser
        return Response(data)


class Registration(generics.GenericAPIView):
    serializer_class = UserRegistrationSerializer
    permission_classes = (AllowAny,)

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer, request.data)

        return Response({}, status=status.HTTP_201_CREATED)

    def perform_create(self, serializer, data):
        user = serializer.create(data)
        return user

[ad:

コメント

タイトルとURLをコピーしました