Обновления в 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.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,
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user