Django: объединить django-allauth и вход по номеру телефона

Как в Django объединить django-allauth и вход по номеру телефона. Мне нужно, чтобы можно было входить либо по номеру телефона, либо через Яндекс. сделать вход по номеру телефону вышло. Но, когда подвязал django-allauth, всё сломалось. Пока не получается реализовать, чтобы работало и то и другое. Пожалуйста распишите подробнее

Имею вот такой код: config/settings.py

""" Django settings for config project.

Generated by 'django-admin startproject' using Django 5.0.6.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/ """ 
import os from pathlib import Path
from dotenv import load_dotenv
load_dotenv()

# Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret! 
SECRET_KEY =


# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True

ALLOWED_HOSTS = []

# Application definition INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'phonenumber_field',

    # Allauth
    'allauth',
    'allauth.account',

    # Social authentication
    'allauth.socialaccount',
    'allauth.socialaccount.providers.yandex',

    # Custom apps
    'main_page',  #added main page app
    'users',
    'catalog',
    'orders',
    'appointments', ]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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',
    "allauth.account.middleware.AccountMiddleware", ]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, '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',
            ],
        },
    }, ]


#Add the link to static files from frontend: STATIC_URL = '/static/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'), ]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    } }


# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    }, ]

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend', ]

AUTH_USER_MODEL = 'users.User' 
AUTHENTICATION_METHOD = 'phone_number'  # Это предложил GPT. Но понятное дело это так не работает, так как значения 'phone_number' быть не может в этих полях
ACCOUNT_AUTHENTICATION_METHOD = 'phone_number' 
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None

# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = 'ru-RU'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True

PHONENUMBER_DEFAULT_REGION = 'RU'

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Я использую пользовательскую модель User, которая определена в приложение users:

import string

from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager 
from django.contrib.auth.models import PermissionsMixin 
from django.db import models 
from django.utils import timezone 
from django.utils.crypto import get_random_string 
from django.utils.translation import gettext_lazy as _ 
from phonenumber_field.modelfields import PhoneNumberField


class UserManager(BaseUserManager):
    """
    Кастомный менеджер для модели User без поля username.
    Предоставляет методы для создания обычных пользователей и суперпользователей.
    """

    def create_user(self, email, phone_number, password=None, **extra_fields):
        """
        Создает и сохраняет обычного пользователя с заданными email, номером телефона и паролем.
        """
        if not phone_number:
            raise ValueError(_('Поле номера телефона должно быть заполнено.'))

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

    def create_superuser(self, email, phone_number, password=None, **extra_fields):
        """
        Создает и сохраняет суперпользователя с заданными email, номером телефона и паролем.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        return self.create_user(email, phone_number, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    """
    Кастомная модель пользователя, использующая номер телефона вместо имени пользователя.
    """
    REPAIR_DATE_CHOICES = [
        ('ongoing', _('Уже идет')),
        ('soon', _('Скоро приступаем')),
        ('six_months', _('В течение полугода')),
        ('one_year', _('В течение года')),
    ]

    email = models.EmailField(null=True, blank=True, unique=True, verbose_name=_('Электронная почта'))
    first_name = models.CharField(max_length=150, verbose_name=_('Имя'))
    last_name = models.CharField(null=True, blank=True, max_length=150, verbose_name=_('Фамилия'))
    patronymic = models.CharField(max_length=150, blank=True, null=True, verbose_name=_('Отчество'))
    birth_date = models.DateField(null=True, blank=True, verbose_name=_('Дата рождения'))
    phone_number = PhoneNumberField(unique=True, verbose_name=_('Номер телефона'))
    phone_is_confirmed = models.BooleanField(_('Телефон подтвержден'), default=False)
    has_children = models.BooleanField(default=False, verbose_name=_('Наличие детей'))
    repair_planned = models.BooleanField(default=False, verbose_name=_('Планируется ли ремонт'))
    repair_date = models.CharField(
        max_length=255,
        choices=REPAIR_DATE_CHOICES,
        null=True,
        blank=True,
        verbose_name=_('Когда планируется ремонт')
    )
    repair_rooms = models.ManyToManyField(
        'Room',
        blank=True,
        verbose_name=_('Комнаты, в которых планируется ремонт')
    )
    subscribe_newsletter = models.BooleanField(default=True, verbose_name=_('Согласие на рассылку'))
    consent_personal_data = models.BooleanField(
        default=True,
        verbose_name=_('Согласие на обработку персональных данных')
    )

    is_staff = models.BooleanField(default=False, verbose_name=_('Является сотрудником'))
    is_active = models.BooleanField(default=True, verbose_name=_('Активный'))
    date_joined = models.DateTimeField(_("Дата регистрации"), default=timezone.now)

    USERNAME_FIELD = 'phone_number'
    REQUIRED_FIELDS = ['email']

    objects = UserManager()

    class Meta:
        verbose_name = _('Пользователь')
        verbose_name_plural = _('Пользователи')

    def __str__(self):
        if self.first_name:
            return f"{self.phone_number} - {self.first_name}"
        return f"{self.phone_number}"


class Room(models.Model):
    """
    Модель для хранения типов помещений.
    """
    REPAIR_ROOMS_CHOICES = [
        ('kitchen', 'Кухня'),
        ('hallway', 'Коридор'),
        ('entryway', 'Прихожая'),
        ('bathroom', 'Ванная'),
        ('children', 'Детская'),
        ('bedroom', 'Спальня'),
        ('living_room', 'Гостиная'),
        ('dining_room', 'Столовая'),
        ('office', 'Кабинет')
    ]
    name = models.CharField(max_length=255, choices=REPAIR_ROOMS_CHOICES, verbose_name=_('Название'))

    class Meta:
        verbose_name = _('Тип помещения')
        verbose_name_plural = _('Типы помещений')

    def __str__(self):
        for key, value in self.REPAIR_ROOMS_CHOICES:
            if key == self.name:
                return value
        return self.name


class LoyaltyProgram(models.Model):
    """
    Модель программы лояльности для пользователей.
    """
    user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('Пользователь'))
    balance = models.IntegerField(default=0, verbose_name=_('Баланс'))
    referral_code = models.CharField(max_length=255, unique=True, verbose_name=_('Реферальный код'))

    class Meta:
        verbose_name = _('Программа лояльности')
        verbose_name_plural = _('Программы лояльности')

    def save(self, *args, **kwargs):
        if not self.referral_code:
            self.referral_code = self.generate_unique_referral_code()

        if self.pk:
            # Получаем старое значение баланса из базы данных
            old_balance = LoyaltyProgram.objects.get(pk=self.pk).balance
            if self.balance != old_balance:
                # Разница между новым и старым балансом
                points_delta = self.balance - old_balance
                # Создаем запись в LoyaltyTransaction
                LoyaltyTransaction.objects.create(
                    user=self.user,
                    points=points_delta,
                )
        else:
            self.balance = 0

        super().save(*args, **kwargs)

    def generate_referral_code(self, length=8):
        characters = string.ascii_uppercase + string.digits
        return get_random_string(length, characters)

    def generate_unique_referral_code(self):
        referral_code = self.generate_referral_code()
        while LoyaltyProgram.objects.filter(referral_code=referral_code).exists():
            referral_code = self.generate_referral_code()
        return referral_code

    def __str__(self):
        return f"{self.user} - {self.referral_code} - {self.balance}"


class LoyaltyTransaction(models.Model):
    """
    Модель транзакции в программе лояльности.
    """
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Пользователь'))
    points = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=0,
        verbose_name=_('Баллы'),
    )
    description = models.CharField(max_length=255, verbose_name=_('Описание'), default='Automated transaction')
    date = models.DateTimeField(auto_now_add=True, verbose_name=_('Дата'))

    class Meta:
        verbose_name = _('Транзакция лояльности')
        verbose_name_plural = _('Транзакции лояльности')

    def __str__(self):
        return f"{self.user} - {self.points} - {self.date}"


# Импорт сигналов from . import signals  # noqa F401 ```

Как правильно всё это сделать? Заранее спасибо!


Ответы (1 шт):

Автор решения: Данила Михеев

В общем отказался я полностью от django-allauth. И сделал на основе стандартной авторизации django вход по номеру телефона, с помощью кода из СМС (ну как в тинькофф банк например).

Вроде всё работает как надо. А реализовал всё это через views.py. Не знаю на сколько такой способ безопасный, но он работает.

from django.contrib.auth import get_user_model, login 
from django.contrib.auth.mixins import LoginRequiredMixin 
from django.shortcuts import render, redirect 
from django.urls import reverse_lazy 
from django.utils.crypto import get_random_string 
from django.views.generic import CreateView, View, UpdateView 
from django.utils.translation import gettext_lazy as _

from .forms import SignUpForm, PhoneNumberForm, PasswordForm, UpdateFirstNameForm

User = get_user_model()


class SignUp(CreateView):
    model = User
    form_class = SignUpForm
    success_url = reverse_lazy('login')
    template_name = 'registration/signup.html'


def login_view(request):
    if request.method == 'POST':
        form = PhoneNumberForm(request.POST)
        if form.is_valid():
            phone_number = form.cleaned_data['phone_number']
            phone_number_str = phone_number.as_e164
            request.session['phone_number'] = phone_number_str

            new_password = get_random_string(length=4, allowed_chars='0123456789')
            request.session['password'] = new_password
            print(f'{phone_number_str} {new_password}: ваш пароль для входа на сайт Istok.')

            return redirect('enter_password')  # Переходим ко второму шагу
        else:
            return render(request, 'registration/login.html', {'form': form})

    # Обработка GET запроса
    form = PhoneNumberForm()
    return render(request, 'registration/login.html', {'form': form})


def password_view(request):
    def generate_new_password():
        new_password = get_random_string(length=4, allowed_chars='0123456789')
        request.session['password'] = new_password
        request.session['attempt_count'] = 0  # Сбрасываем счетчик попыток
        print(f'{phone_number_str} {new_password} ваш пароль для входа на сайт Istok.')

    if 'phone_number' not in request.session:
        return redirect('login')  # Если нет номера телефона в сессии, переходим на первый шаг

    if request.session['password'] is None:
        generate_new_password()

    if request.method == 'POST':
        form = PasswordForm(request.POST)
        if form.is_valid():
            password = form.cleaned_data['password']
            phone_number_str = request.session['phone_number']

            # Проверяем, является ли введенный пароль сгенерированным паролем из сессии
            if password == request.session['password']:
                # Пытаемся аутентифицировать пользователя
                user = User.objects.filter(phone_number=phone_number_str).first()
                if user is not None:
                    login(request, user)  # Логин пользователя
                    del request.session['phone_number']  # Удаляем phone_number из сессии после успешной авторизации
                    del request.session['password']  # Удаляем password из сессии после успешной авторизации
                    return redirect('main_page_index')
                else:
                    # Если пользователь не существует, создаем нового
                    new_user = User.objects.create_user(phone_number=phone_number_str)
                    login(request, new_user)  # Логин созданного пользователя
                    del request.session['phone_number']  # Удаляем phone_number из сессии после успешной авторизации
                    del request.session['password']  # Удаляем password из сессии после успешной авторизации
                    return redirect('main_page_index')

            else:
                error_message = _('Неверный пароль.')
                # Увеличиваем счетчик попыток
                request.session['attempt_count'] = request.session.get('attempt_count', 0) + 1

                # Если количество попыток достигло 10, генерируем новый пароль
                if request.session['attempt_count'] == 10:
                    generate_new_password()

                return render(request, 'registration/login_sms.html', {'form': form, 'error_message': error_message})

    else:
        form = PasswordForm()

    return render(request, 'registration/login_sms.html', {'form': form})


class UpdateFirstNameView(LoginRequiredMixin, UpdateView):
    model = User
    form_class = UpdateFirstNameForm
    template_name = 'registration/update_first_name.html'
    success_url = reverse_lazy('main_page_index')

    def get_object(self, queryset=None):
        return self.request.user

    def form_valid(self, form):
        response = super().form_valid(form)
        # Save the user's first name
        self.request.user.first_name = form.cleaned_data['first_name']
        self.request.user.save()
        return response ```

forms.py

from django import forms 
from django.contrib.auth import get_user_model 
from django.contrib.auth.forms import UserCreationForm 
from django.utils.translation import gettext_lazy as _ 
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.widgets import RegionalPhoneNumberWidget

User = get_user_model()


class PhoneNumberForm(forms.Form):
    phone_number = PhoneNumberField(
        label='',
        required=True,
        widget=RegionalPhoneNumberWidget(attrs={
            'class': 'login__input',
            'placeholder': '+7 (999) 999-99-99'
        })
    )


class PasswordForm(forms.Form):
    password = forms.CharField(
        label='',
        required=True,
        widget=forms.PasswordInput(attrs={
            'class': 'login__input',
            'placeholder': 'Введите пароль'
        })
    )

    def clean_password(self):
        password = self.cleaned_data.get('password')
        if len(password) != 4 or not password.isdigit():
            raise forms.ValidationError('Пароль должен состоять из 4 цифр.')
        return password


class SignUpForm(UserCreationForm):
    phone_number = PhoneNumberField(label=_('Номер телефона'), required=True)

    class Meta:
        model = User
        fields = ('phone_number', 'password1', 'password2')


class UpdateFirstNameForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['first_name']
        widgets = {
            'first_name': forms.TextInput(attrs={'class': 'login__input'}),
        }
        labels = {
            'first_name': '',
        } ```

Тут выводится в консоль, но там уже переделать на смс не составит труда. На сколько приемлемая такая реализация?

→ Ссылка