Обновления в tenants: админ, модели, миграции и сервисы

This commit is contained in:
2026-01-08 22:13:20 +03:00
parent 7f91244d63
commit 76acf419fc
4 changed files with 100 additions and 34 deletions

View File

@@ -176,12 +176,20 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
def reject_registrations(self, request, queryset): def reject_registrations(self, request, queryset):
"""Массовое отклонение заявок""" """Массовое отклонение заявок"""
rejected_count = queryset.filter(status=TenantRegistration.STATUS_PENDING).update( from platform_admin.models import PlatformAdmin
status=TenantRegistration.STATUS_REJECTED,
processed_at=timezone.now(), # Получаем processed_by только если это PlatformAdmin
processed_by=request.user, processed_by = request.user if isinstance(request.user, PlatformAdmin) else None
rejection_reason="Отклонено массовым действием"
) # Обновляем каждую заявку индивидуально (нельзя использовать update с FK на другую модель)
rejected_count = 0
for registration in queryset.filter(status=TenantRegistration.STATUS_PENDING):
registration.status = TenantRegistration.STATUS_REJECTED
registration.processed_at = timezone.now()
registration.processed_by = processed_by
registration.rejection_reason = "Отклонено массовым действием"
registration.save()
rejected_count += 1
if rejected_count > 0: if rejected_count > 0:
messages.success(request, f"Отклонено заявок: {rejected_count}") messages.success(request, f"Отклонено заявок: {rejected_count}")
@@ -251,10 +259,13 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
messages.error(request, f"Ошибка при активации: {str(e)}") messages.error(request, f"Ошибка при активации: {str(e)}")
elif 'reject' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: elif 'reject' in request.GET and registration.status == TenantRegistration.STATUS_PENDING:
from platform_admin.models import PlatformAdmin
# Обрабатываем отклонение # Обрабатываем отклонение
registration.status = TenantRegistration.STATUS_REJECTED registration.status = TenantRegistration.STATUS_REJECTED
registration.processed_at = timezone.now() registration.processed_at = timezone.now()
registration.processed_by = request.user # processed_by только если это PlatformAdmin
registration.processed_by = request.user if isinstance(request.user, PlatformAdmin) else None
registration.rejection_reason = "Отклонено администратором" registration.rejection_reason = "Отклонено администратором"
registration.save() registration.save()
messages.warning(request, f"Заявка '{registration.shop_name}' отклонена.") messages.warning(request, f"Заявка '{registration.shop_name}' отклонена.")

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:56
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
@@ -84,7 +84,7 @@ class Migration(migrations.Migration):
('password_setup_token', models.UUIDField(blank=True, help_text='UUID токен для ссылки установки пароля владельцем', null=True, unique=True, verbose_name='Токен установки пароля')), ('password_setup_token', models.UUIDField(blank=True, help_text='UUID токен для ссылки установки пароля владельцем', null=True, unique=True, verbose_name='Токен установки пароля')),
('password_setup_token_created_at', models.DateTimeField(blank=True, help_text='Когда был создан токен установки пароля (действителен 7 дней)', null=True, verbose_name='Дата создания токена')), ('password_setup_token_created_at', models.DateTimeField(blank=True, help_text='Когда был создан токен установки пароля (действителен 7 дней)', null=True, verbose_name='Дата создания токена')),
('owner_notified_at', models.DateTimeField(blank=True, help_text='Когда было отправлено письмо владельцу с ссылкой установки пароля', null=True, verbose_name='Дата уведомления владельца')), ('owner_notified_at', models.DateTimeField(blank=True, help_text='Когда было отправлено письмо владельцу с ссылкой установки пароля', null=True, verbose_name='Дата уведомления владельца')),
('processed_by', models.ForeignKey(blank=True, help_text='Администратор, который обработал заявку', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Обработал')), ('processed_by', models.ForeignKey(blank=True, help_text='Администратор платформы, который обработал заявку', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Обработал')),
('tenant', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tenants.client', verbose_name='Созданный тенант')), ('tenant', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tenants.client', verbose_name='Созданный тенант')),
], ],
options={ options={

View File

@@ -160,12 +160,12 @@ class TenantRegistration(models.Model):
) )
processed_by = models.ForeignKey( processed_by = models.ForeignKey(
settings.AUTH_USER_MODEL, 'platform_admin.PlatformAdmin',
null=True, null=True,
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name="Обработал", verbose_name="Обработал",
help_text="Администратор, который обработал заявку" help_text="Администратор платформы, который обработал заявку"
) )
rejection_reason = models.TextField( rejection_reason = models.TextField(

View File

@@ -5,10 +5,10 @@
Единственный источник истины для создания тенанта и всех связанных сущностей. Единственный источник истины для создания тенанта и всех связанных сущностей.
""" """
import logging import logging
import secrets
from django.db import connection, transaction from django.db import connection, transaction
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management import call_command from django.core.management import call_command
from tenants.models import Client, Domain, TenantRegistration, Subscription from tenants.models import Client, Domain, TenantRegistration, Subscription
@@ -25,7 +25,7 @@ class TenantOnboardingService:
- Создание Domain (домен) - Создание Domain (домен)
- Создание Subscription (подписка) - Создание Subscription (подписка)
- Миграции схемы - Миграции схемы
- Создание пользователей (admin, owner) - Создание владельца (owner)
- Инициализация системных данных - Инициализация системных данных
""" """
@@ -63,7 +63,8 @@ class TenantOnboardingService:
# 5. Создание подписки # 5. Создание подписки
subscription = cls._create_subscription(client) subscription = cls._create_subscription(client)
# 6. Инициализация данных тенанта (в контексте схемы) # 6. Инициализация данных тенанта (в контексте tenant schema)
# CustomUser теперь в TENANT_APPS - создаётся в схеме тенанта
connection.set_tenant(client) connection.set_tenant(client)
try: try:
cls._init_tenant_users(registration) cls._init_tenant_users(registration)
@@ -71,7 +72,7 @@ class TenantOnboardingService:
finally: finally:
connection.set_schema_to_public() connection.set_schema_to_public()
# 7. Отправка email владельцу # 8. Отправка email владельцу
cls._send_welcome_email(registration) cls._send_welcome_email(registration)
# 8. Обновление статуса заявки # 8. Обновление статуса заявки
@@ -199,28 +200,23 @@ class TenantOnboardingService:
@classmethod @classmethod
def _init_tenant_users(cls, registration: TenantRegistration): def _init_tenant_users(cls, registration: TenantRegistration):
"""Создание пользователей тенанта (admin и owner).""" """
User = get_user_model() Создание пользователей тенанта: владелец + техподдержка платформы.
# Суперпользователь (системный admin) ВАЖНО: CustomUser в TENANT_APPS - создаётся в схеме тенанта.
if not User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).exists(): Вызывается в контексте tenant schema (connection.set_tenant уже выполнен).
superuser = User.objects.create_superuser(
email=settings.TENANT_ADMIN_EMAIL,
name=settings.TENANT_ADMIN_NAME,
password=settings.TENANT_ADMIN_PASSWORD
)
logger.info(f"Суперпользователь создан: {superuser.id}")
else:
logger.warning(f"Пользователь {settings.TENANT_ADMIN_EMAIL} уже существует")
# Владелец тенанта Django Admin доступна только для PlatformAdmin (из public schema).
if not User.objects.filter(email=registration.owner_email).exists(): CustomUser используют только фронтенд.
owner = User.objects.create_user( """
from accounts.models import CustomUser
# 1. Владелец тенанта
if not CustomUser.objects.filter(email=registration.owner_email).exists():
owner = CustomUser.objects.create_user(
email=registration.owner_email, email=registration.owner_email,
name=registration.owner_name, name=registration.owner_name,
password=None, # Пароль будет установлен через ссылку password=None, # Пароль будет установлен через ссылку
is_staff=False,
is_superuser=False
) )
owner.is_email_confirmed = True owner.is_email_confirmed = True
owner.email_confirmed_at = timezone.now() owner.email_confirmed_at = timezone.now()
@@ -228,11 +224,14 @@ class TenantOnboardingService:
owner.save() owner.save()
logger.info(f"Аккаунт владельца создан: {owner.id}") logger.info(f"Аккаунт владельца создан: {owner.id}")
# Роли # Назначаем роль owner
cls._assign_owner_role(owner) cls._assign_owner_role(owner)
else: else:
logger.warning(f"Пользователь {registration.owner_email} уже существует") logger.warning(f"Пользователь {registration.owner_email} уже существует")
# 2. Техподдержка платформы (скрытый аккаунт)
cls._create_platform_support_user(registration)
@classmethod @classmethod
def _assign_owner_role(cls, owner): def _assign_owner_role(cls, owner):
"""Назначение роли owner владельцу тенанта.""" """Назначение роли owner владельцу тенанта."""
@@ -246,6 +245,51 @@ class TenantOnboardingService:
except Exception as e: except Exception as e:
logger.error(f"Ошибка при назначении роли: {e}", exc_info=True) logger.error(f"Ошибка при назначении роли: {e}", exc_info=True)
@classmethod
def _create_platform_support_user(cls, registration: TenantRegistration):
"""
Создание скрытого аккаунта техподдержки платформы.
- Email берётся из настроек PLATFORM_SUPPORT_EMAIL
- Пароль генерируется уникальный для каждого тенанта
- Пароль выводится в лог (для владельца платформы)
- Пользователь не виден владельцу тенанта
"""
from accounts.models import CustomUser
from user_roles.services import RoleService
from user_roles.models import Role
support_email = getattr(settings, 'PLATFORM_SUPPORT_EMAIL', None)
if not support_email:
logger.info("PLATFORM_SUPPORT_EMAIL не задан - пропускаем создание техподдержки")
return
if CustomUser.objects.filter(email=support_email).exists():
logger.info(f"Техподдержка {support_email} уже существует в этом тенанте")
return
# Генерируем уникальный пароль для этого тенанта
password = secrets.token_urlsafe(16)
support_user = CustomUser.objects.create_user(
email=support_email,
name='Техподдержка',
password=password,
)
support_user.is_email_confirmed = True
support_user.email_confirmed_at = timezone.now()
support_user.is_active = True
support_user.save()
# Назначаем роль platform_support
RoleService.assign_role_to_user(support_user, Role.PLATFORM_SUPPORT, created_by=None)
# Выводим пароль в лог (безопасно, т.к. логи доступны только владельцу платформы)
logger.info(
f"[PLATFORM_SUPPORT] Тенант: {registration.schema_name} | "
f"Email: {support_email} | Пароль: {password}"
)
@classmethod @classmethod
def _init_tenant_data(cls): def _init_tenant_data(cls):
"""Инициализация системных данных тенанта.""" """Инициализация системных данных тенанта."""
@@ -271,7 +315,18 @@ class TenantOnboardingService:
"""Финализация заявки на регистрацию.""" """Финализация заявки на регистрацию."""
registration.status = TenantRegistration.STATUS_APPROVED registration.status = TenantRegistration.STATUS_APPROVED
registration.processed_at = timezone.now() registration.processed_at = timezone.now()
registration.processed_by = admin_user
# admin_user теперь должен быть PlatformAdmin
if admin_user is not None:
from platform_admin.models import PlatformAdmin
if isinstance(admin_user, PlatformAdmin):
registration.processed_by = admin_user
else:
logger.warning(f"admin_user не является PlatformAdmin: {type(admin_user)}")
registration.processed_by = None
else:
registration.processed_by = None
registration.tenant = client registration.tenant = client
registration.save() registration.save()
logger.info(f"Заявка {registration.id} финализирована") logger.info(f"Заявка {registration.id} финализирована")