From 76acf419fca5553619442ff9f3cfdb7d7d57055e Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Thu, 8 Jan 2026 22:13:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=20tenants:=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD,=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8,=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myproject/tenants/admin.py | 25 +++-- myproject/tenants/migrations/0001_initial.py | 4 +- myproject/tenants/models.py | 4 +- myproject/tenants/services/onboarding.py | 101 ++++++++++++++----- 4 files changed, 100 insertions(+), 34 deletions(-) diff --git a/myproject/tenants/admin.py b/myproject/tenants/admin.py index 152f1d0..85a0037 100644 --- a/myproject/tenants/admin.py +++ b/myproject/tenants/admin.py @@ -176,12 +176,20 @@ class TenantRegistrationAdmin(admin.ModelAdmin): def reject_registrations(self, request, queryset): """Массовое отклонение заявок""" - rejected_count = queryset.filter(status=TenantRegistration.STATUS_PENDING).update( - status=TenantRegistration.STATUS_REJECTED, - processed_at=timezone.now(), - processed_by=request.user, - rejection_reason="Отклонено массовым действием" - ) + from platform_admin.models import PlatformAdmin + + # Получаем processed_by только если это PlatformAdmin + processed_by = request.user if isinstance(request.user, PlatformAdmin) else None + + # Обновляем каждую заявку индивидуально (нельзя использовать 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: messages.success(request, f"Отклонено заявок: {rejected_count}") @@ -251,10 +259,13 @@ class TenantRegistrationAdmin(admin.ModelAdmin): messages.error(request, f"Ошибка при активации: {str(e)}") elif 'reject' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: + from platform_admin.models import PlatformAdmin + # Обрабатываем отклонение registration.status = TenantRegistration.STATUS_REJECTED 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.save() messages.warning(request, f"Заявка '{registration.shop_name}' отклонена.") diff --git a/myproject/tenants/migrations/0001_initial.py b/myproject/tenants/migrations/0001_initial.py index ee3409f..80326dc 100644 --- a/myproject/tenants/migrations/0001_initial.py +++ b/myproject/tenants/migrations/0001_initial.py @@ -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.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_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='Дата уведомления владельца')), - ('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='Созданный тенант')), ], options={ diff --git a/myproject/tenants/models.py b/myproject/tenants/models.py index c8c6369..e927028 100644 --- a/myproject/tenants/models.py +++ b/myproject/tenants/models.py @@ -160,12 +160,12 @@ class TenantRegistration(models.Model): ) processed_by = models.ForeignKey( - settings.AUTH_USER_MODEL, + 'platform_admin.PlatformAdmin', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Обработал", - help_text="Администратор, который обработал заявку" + help_text="Администратор платформы, который обработал заявку" ) rejection_reason = models.TextField( diff --git a/myproject/tenants/services/onboarding.py b/myproject/tenants/services/onboarding.py index 99bd841..8478925 100644 --- a/myproject/tenants/services/onboarding.py +++ b/myproject/tenants/services/onboarding.py @@ -5,10 +5,10 @@ Единственный источник истины для создания тенанта и всех связанных сущностей. """ import logging +import secrets from django.db import connection, transaction from django.utils import timezone from django.conf import settings -from django.contrib.auth import get_user_model from django.core.management import call_command from tenants.models import Client, Domain, TenantRegistration, Subscription @@ -25,7 +25,7 @@ class TenantOnboardingService: - Создание Domain (домен) - Создание Subscription (подписка) - Миграции схемы - - Создание пользователей (admin, owner) + - Создание владельца (owner) - Инициализация системных данных """ @@ -63,7 +63,8 @@ class TenantOnboardingService: # 5. Создание подписки subscription = cls._create_subscription(client) - # 6. Инициализация данных тенанта (в контексте схемы) + # 6. Инициализация данных тенанта (в контексте tenant schema) + # CustomUser теперь в TENANT_APPS - создаётся в схеме тенанта connection.set_tenant(client) try: cls._init_tenant_users(registration) @@ -71,7 +72,7 @@ class TenantOnboardingService: finally: connection.set_schema_to_public() - # 7. Отправка email владельцу + # 8. Отправка email владельцу cls._send_welcome_email(registration) # 8. Обновление статуса заявки @@ -199,28 +200,23 @@ class TenantOnboardingService: @classmethod def _init_tenant_users(cls, registration: TenantRegistration): - """Создание пользователей тенанта (admin и owner).""" - User = get_user_model() + """ + Создание пользователей тенанта: владелец + техподдержка платформы. - # Суперпользователь (системный admin) - if not User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).exists(): - 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} уже существует") + ВАЖНО: CustomUser в TENANT_APPS - создаётся в схеме тенанта. + Вызывается в контексте tenant schema (connection.set_tenant уже выполнен). - # Владелец тенанта - if not User.objects.filter(email=registration.owner_email).exists(): - owner = User.objects.create_user( + Django Admin доступна только для PlatformAdmin (из public schema). + CustomUser используют только фронтенд. + """ + 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, name=registration.owner_name, password=None, # Пароль будет установлен через ссылку - is_staff=False, - is_superuser=False ) owner.is_email_confirmed = True owner.email_confirmed_at = timezone.now() @@ -228,11 +224,14 @@ class TenantOnboardingService: owner.save() logger.info(f"Аккаунт владельца создан: {owner.id}") - # Роли + # Назначаем роль owner cls._assign_owner_role(owner) else: logger.warning(f"Пользователь {registration.owner_email} уже существует") + # 2. Техподдержка платформы (скрытый аккаунт) + cls._create_platform_support_user(registration) + @classmethod def _assign_owner_role(cls, owner): """Назначение роли owner владельцу тенанта.""" @@ -246,6 +245,51 @@ class TenantOnboardingService: except Exception as e: 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 def _init_tenant_data(cls): """Инициализация системных данных тенанта.""" @@ -271,7 +315,18 @@ class TenantOnboardingService: """Финализация заявки на регистрацию.""" registration.status = TenantRegistration.STATUS_APPROVED 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.save() logger.info(f"Заявка {registration.id} финализирована")