Обновления в accounts: модели, представления, миграции и новый бэкенд

This commit is contained in:
2026-01-08 22:09:38 +03:00
parent dbf00dab29
commit 796fd8fe18
4 changed files with 139 additions and 15 deletions

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
"""
Authentication backend для CustomUser (пользователей тенантов).
Этот backend используется для аутентификации пользователей магазинов.
Работает ТОЛЬКО на tenant доменах, НЕ на public домене.
ВАЖНО: CustomUser теперь в TENANT_APPS - каждый тенант имеет свою таблицу!
Backend работает с таблицей accounts_customuser в текущей tenant schema.
"""
from django.contrib.auth.backends import ModelBackend
from django.db import connection
class TenantUserBackend(ModelBackend):
"""
Backend аутентификации для CustomUser (tenant-only).
Особенности:
- Работает ТОЛЬКО на tenant доменах (не на public)
- Ищет пользователя в таблице accounts_customuser текущей tenant schema
- Один email в разных тенантах = разные записи в разных таблицах БД
Пользователь из tenant A физически не существует в tenant B.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
Аутентификация CustomUser по email и паролю.
Args:
request: HTTP запрос
username: Email пользователя
password: Пароль
Returns:
CustomUser если аутентификация успешна, иначе None
"""
# Не работает на public домене
schema_name = getattr(connection, 'schema_name', 'public')
if schema_name == 'public':
return None
if username is None or password is None:
return None
# Импортируем напрямую, не через get_user_model()
# т.к. AUTH_USER_MODEL теперь PlatformAdmin
from accounts.models import CustomUser
try:
# django-tenants автоматически направляет запрос в текущую schema
user = CustomUser.objects.get(email=username)
except CustomUser.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user
CustomUser().set_password(password)
return None
if not user.check_password(password):
return None
if not self.user_can_authenticate(user):
return None
return user
def get_user(self, user_id):
"""
Получение CustomUser по ID.
На public домене возвращает None.
"""
schema_name = getattr(connection, 'schema_name', 'public')
if schema_name == 'public':
return None
from accounts.models import CustomUser
try:
return CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
return None
def user_can_authenticate(self, user):
"""
Проверка что пользователь активен.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2026-01-03 23:23 # Generated by Django 5.0.10 on 2026-01-08 15:58
import django.contrib.auth.validators import django.contrib.auth.validators
import django.utils.timezone import django.utils.timezone
@@ -38,9 +38,8 @@ class Migration(migrations.Migration):
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')),
], ],
options={ options={
'verbose_name': 'user', 'verbose_name': 'Пользователь магазина',
'verbose_name_plural': 'users', 'verbose_name_plural': 'Пользователи магазина',
'abstract': False,
}, },
), ),
] ]

View File

@@ -43,8 +43,18 @@ class CustomUserManager(BaseUserManager):
class CustomUser(AbstractUser): class CustomUser(AbstractUser):
"""
Пользователь тенанта (магазина).
ВАЖНО: Эта модель в TENANT_APPS - каждый тенант имеет свою таблицу!
Один email в разных тенантах = разные записи в разных схемах БД.
Полная изоляция обеспечивается на уровне PostgreSQL schemas.
НЕ является AUTH_USER_MODEL (это PlatformAdmin).
"""
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
is_email_confirmed = models.BooleanField(default=False) is_email_confirmed = models.BooleanField(default=False)
email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
email_confirmed_at = models.DateTimeField(null=True, blank=True) email_confirmed_at = models.DateTimeField(null=True, blank=True)
@@ -53,7 +63,7 @@ class CustomUser(AbstractUser):
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name'] REQUIRED_FIELDS = ['name']
objects = CustomUserManager() # Добавляем кастомный менеджер objects = CustomUserManager()
# Изменяем related_name для избежания конфликта с встроенной моделью User # Изменяем related_name для избежания конфликта с встроенной моделью User
groups = models.ManyToManyField( groups = models.ManyToManyField(
@@ -71,6 +81,10 @@ class CustomUser(AbstractUser):
help_text='Specific permissions for this user.', help_text='Specific permissions for this user.',
) )
class Meta:
verbose_name = "Пользователь магазина"
verbose_name_plural = "Пользователи магазина"
def __str__(self): def __str__(self):
return self.email return self.email

View File

@@ -11,12 +11,29 @@ from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.db import connection
from .forms import PasswordResetForm from .forms import PasswordResetForm
from .models import CustomUser from .models import CustomUser
import uuid import uuid
def login_view(request): def login_view(request):
"""
Страница входа для пользователей тенанта (CustomUser).
SECURITY: Работает ТОЛЬКО на tenant доменах!
На public домене перенаправляет на страницу логина PlatformAdmin.
"""
# Проверяем что мы НЕ на public домене
schema_name = getattr(connection, 'schema_name', 'public')
if schema_name == 'public':
messages.info(
request,
'Вход для пользователей магазинов доступен только на домене вашего магазина. '
'Если вы администратор платформы, используйте /platform/login/'
)
return redirect('platform_admin:login')
if request.method == 'POST': if request.method == 'POST':
email = request.POST.get('email') email = request.POST.get('email')
password = request.POST.get('password') password = request.POST.get('password')
@@ -25,13 +42,17 @@ def login_view(request):
user = authenticate(request, username=email, password=password) user = authenticate(request, username=email, password=password)
if user is not None: if user is not None:
if user.is_email_confirmed: # Проверяем, подтвержден ли email # Проверяем, что это CustomUser (пользователь магазина), а не PlatformAdmin
login(request, user) if not isinstance(user, CustomUser):
# Не раскрываем информацию о существовании других типов пользователей
messages.error(request, 'Пользователь не найден.')
elif not user.is_email_confirmed:
messages.error(request, 'Пожалуйста, подтвердите ваш email для входа.')
else:
login(request, user, backend='accounts.backends.TenantUserBackend')
# Перенаправляем на главную страницу после успешного входа # Перенаправляем на главную страницу после успешного входа
next_page = request.GET.get('next', 'index') # Если есть параметр next, переходим туда next_page = request.GET.get('next', 'index') # Если есть параметр next, переходим туда
return redirect(next_page) return redirect(next_page)
else:
messages.error(request, 'Пожалуйста, подтвердите ваш email для входа.')
else: else:
messages.error(request, 'Неверный email или пароль.') messages.error(request, 'Неверный email или пароль.')
@@ -203,9 +224,9 @@ def password_setup_confirm(request, token):
registration.password_setup_token_created_at = None registration.password_setup_token_created_at = None
registration.save() registration.save()
# Автоматический вход # Автоматический вход (используем TenantUserBackend)
connection.set_tenant(tenant) connection.set_tenant(tenant)
login(request, owner, backend='django.contrib.auth.backends.ModelBackend') login(request, owner, backend='accounts.backends.TenantUserBackend')
messages.success( messages.success(
request, request,