Обновления в accounts: модели, представления, миграции и новый бэкенд
This commit is contained in:
90
myproject/accounts/backends.py
Normal file
90
myproject/accounts/backends.py
Normal 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
|
||||
@@ -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': 'Пользователи магазина',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -43,17 +43,27 @@ 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)
|
||||
password_reset_token = models.UUIDField(null=True, blank=True, editable=False, unique=True)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -11,30 +11,51 @@ 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')
|
||||
|
||||
|
||||
# Используем email как логин
|
||||
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 или пароль.')
|
||||
|
||||
|
||||
return render(request, 'login.html')
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user