Обновления в tenants: админ, модели, миграции и сервисы
This commit is contained in:
@@ -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}' отклонена.")
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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} финализирована")
|
||||
|
||||
Reference in New Issue
Block a user