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