Обновления в 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.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')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
'verbose_name': 'Пользователь магазина',
'verbose_name_plural': 'Пользователи магазина',
},
),
]

View File

@@ -43,8 +43,18 @@ class CustomUserManager(BaseUserManager):
class CustomUser(AbstractUser):
"""
Пользователь тенанта (магазина).
ВАЖНО: Эта модель в TENANT_APPS - каждый тенант имеет свою таблицу!
Один email в разных тенантах = разные записи в разных схемах БД.
Полная изоляция обеспечивается на уровне PostgreSQL schemas.
НЕ является AUTH_USER_MODEL (это PlatformAdmin).
"""
email = models.EmailField(unique=True)
name = models.CharField(max_length=100)
is_email_confirmed = models.BooleanField(default=False)
email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
email_confirmed_at = models.DateTimeField(null=True, blank=True)
@@ -53,7 +63,7 @@ class CustomUser(AbstractUser):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name']
objects = CustomUserManager() # Добавляем кастомный менеджер
objects = CustomUserManager()
# Изменяем related_name для избежания конфликта с встроенной моделью User
groups = models.ManyToManyField(
@@ -71,6 +81,10 @@ class CustomUser(AbstractUser):
help_text='Specific permissions for this user.',
)
class Meta:
verbose_name = "Пользователь магазина"
verbose_name_plural = "Пользователи магазина"
def __str__(self):
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 import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.db import connection
from .forms import PasswordResetForm
from .models import CustomUser
import uuid
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':
email = request.POST.get('email')
password = request.POST.get('password')
@@ -25,13 +42,17 @@ def login_view(request):
user = authenticate(request, username=email, password=password)
if user is not None:
if user.is_email_confirmed: # Проверяем, подтвержден ли email
login(request, user)
# Проверяем, что это CustomUser (пользователь магазина), а не PlatformAdmin
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, переходим туда
return redirect(next_page)
else:
messages.error(request, 'Пожалуйста, подтвердите ваш email для входа.')
else:
messages.error(request, 'Неверный email или пароль.')
@@ -203,9 +224,9 @@ def password_setup_confirm(request, token):
registration.password_setup_token_created_at = None
registration.save()
# Автоматический вход
# Автоматический вход (используем TenantUserBackend)
connection.set_tenant(tenant)
login(request, owner, backend='django.contrib.auth.backends.ModelBackend')
login(request, owner, backend='accounts.backends.TenantUserBackend')
messages.success(
request,