Как в Django объединить django-allauth и вход по номеру телефона. Мне нужно, чтобы можно было входить либо по номеру телефона, либо через Яндекс
Пытаюсь реализовать вход по номеру телефона и через яндекс. Просто сделать вход по номеру телефону вышло. Но когда подвязал django-allauth, то всё сломалось. Уже долго бьюсь над этим, пока не получается реализовать, чтобы работало и то и другое. Долго искал, так и не нашёл Я в этом всём начинающий. Пожалуйста распишите подробнее
Имею вот такой код: config/settings.py
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 = 'django-insecure-j)dtir$%isj1i6mnzz2$bk5)6o(k=28ay2*$p&o0pljma4v=bk' # 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:
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 ```
Как правильно всё это сделать? Заранее спасибо😌
В общем отказался я полностью от 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': '', } ```
Тут выводится в консоль, но там уже переделать на смс не составит труда. На сколько приемлемая такая реализация?