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': '', } ```
Тут выводится в консоль, но там уже переделать на смс не составит труда. На сколько приемлемая такая реализация?