Files
octopus/myproject/tenants/services/onboarding.py
Andrey Smakotin b562eabcaf refactor(admin): удален избыточный admin_access_middleware
- Удален TenantAdminAccessMiddleware (избыточен — Django Admin уже проверяет is_staff)
- CustomUser.create_superuser теперь устанавливает is_staff=False (нет доступа к /admin/)
- PlatformAdmin с is_staff=True сохраняет доступ к админке
- Обновлен комментарий в onboarding.py

Доступ к /admin/ теперь контролируется стандартным механизмом Django Admin
через has_permission() + существующие authentication backends.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:00:14 +03:00

291 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Сервис онбординга новых тенантов.
Единственный источник истины для создания тенанта и всех связанных сущностей.
"""
import logging
from django.db import connection, transaction
from django.utils import timezone
from django.conf import settings
from django.core.management import call_command
from tenants.models import Client, Domain, TenantRegistration, Subscription
logger = logging.getLogger(__name__)
class TenantOnboardingService:
"""
Сервис онбординга нового тенанта.
Отвечает за полный цикл создания тенанта:
- Создание Client (тенант)
- Создание Domain (домен)
- Создание Subscription (подписка)
- Миграции схемы
- Создание владельца (owner)
- Инициализация системных данных
"""
@classmethod
@transaction.atomic
def activate_registration(cls, registration: TenantRegistration, admin_user=None) -> Client:
"""
Полная активация заявки на регистрацию тенанта.
Args:
registration: Заявка на регистрацию
admin_user: Администратор, одобривший заявку (опционально)
Returns:
Client: Созданный тенант
Raises:
ValueError: Если тенант уже существует
"""
logger.info(f"Начало активации заявки: {registration.schema_name}")
# 1. Проверка уникальности
if Client.objects.filter(schema_name=registration.schema_name).exists():
raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!")
# 2. Создание тенанта
client = cls._create_client(registration)
# 3. Создание домена
domain = cls._create_domain(client, registration)
# 4. Применение миграций
cls._run_migrations(client)
# 5. Создание подписки
subscription = cls._create_subscription(client)
# 6. Инициализация данных тенанта (в контексте tenant schema)
# CustomUser теперь в TENANT_APPS - создаётся в схеме тенанта
connection.set_tenant(client)
try:
cls._init_tenant_users(registration)
cls._init_tenant_data()
finally:
connection.set_schema_to_public()
# 8. Отправка email владельцу
cls._send_welcome_email(registration)
# 8. Обновление статуса заявки
cls._finalize_registration(registration, client, admin_user)
logger.info(f"Заявка {registration.id} успешно активирована. Тенант: {client.id}")
return client
@classmethod
def init_tenant_data(cls, reset: bool = False):
"""
Инициализация системных данных тенанта.
Вызывается в контексте схемы тенанта (connection.set_tenant уже выполнен).
Args:
reset: Если True, удаляет и пересоздаёт данные
"""
from customers.models import Customer
from orders.services import OrderStatusService, PaymentMethodService
from inventory.services import WarehouseService, ShowcaseService
from products.services import UnitOfMeasureService
# 1. Системный клиент
logger.info("Создание системного клиента...")
if reset:
Customer.objects.filter(is_system_customer=True).delete()
customer, created = Customer.get_or_create_system_customer()
logger.info(f"Системный клиент: {'создан' if created else 'уже существует'}")
# 2. Статусы заказов
logger.info("Создание статусов заказов...")
if reset:
from orders.models import OrderStatus
OrderStatus.objects.filter(is_system=True).delete()
OrderStatusService.create_default_statuses()
logger.info("Статусы заказов созданы")
# 3. Способы оплаты
logger.info("Создание способов оплаты...")
if reset:
PaymentMethodService.reset_default_methods()
else:
PaymentMethodService.create_default_methods()
logger.info("Способы оплаты созданы")
# 4. Склад по умолчанию
logger.info("Создание склада по умолчанию...")
if reset:
warehouse = WarehouseService.reset_default()
else:
warehouse, created = WarehouseService.get_or_create_default()
logger.info(f"Склад по умолчанию: {warehouse.name}")
# 5. Витрина по умолчанию
logger.info("Создание витрины по умолчанию...")
if reset:
showcase = ShowcaseService.reset_default(warehouse)
else:
showcase, created = ShowcaseService.get_or_create_default(warehouse)
logger.info(f"Витрина по умолчанию: {showcase.name}")
# 6. Единицы измерения
logger.info("Создание единиц измерения...")
if reset:
UnitOfMeasureService.reset_default_units()
else:
UnitOfMeasureService.create_default_units()
logger.info("Единицы измерения созданы")
# ==================== Приватные методы ====================
@classmethod
def _create_client(cls, registration: TenantRegistration) -> Client:
"""Создание тенанта (Client)."""
logger.info(f"Создание тенанта: {registration.schema_name}")
client = Client.objects.create(
schema_name=registration.schema_name,
name=registration.shop_name,
owner_email=registration.owner_email,
owner_name=registration.owner_name,
phone=registration.phone,
is_active=True
)
logger.info(f"Тенант создан: {client.id}")
return client
@classmethod
def _create_domain(cls, client: Client, registration: TenantRegistration) -> Domain:
"""Создание домена для тенанта."""
domain_base = settings.TENANT_DOMAIN_BASE
# Убираем порт из домена (django-tenants ищет по hostname без порта)
if ':' in domain_base:
domain_base = domain_base.split(':')[0]
domain_name = f"{registration.schema_name}.{domain_base}"
logger.info(f"Создание домена: {domain_name}")
domain = Domain.objects.create(
domain=domain_name,
tenant=client,
is_primary=True
)
logger.info(f"Домен создан: {domain.id}")
return domain
@classmethod
def _run_migrations(cls, client: Client):
"""Применение миграций для схемы тенанта."""
logger.info(f"Применение миграций для тенанта: {client.schema_name}")
try:
call_command('migrate_schemas', schema_name=client.schema_name, verbosity=1)
logger.info("Миграции успешно применены")
except Exception as e:
logger.error(f"Ошибка при применении миграций: {e}", exc_info=True)
raise
@classmethod
def _create_subscription(cls, client: Client) -> Subscription:
"""Создание триальной подписки."""
logger.info(f"Создание триальной подписки для тенанта: {client.id}")
subscription = Subscription.create_trial(client)
logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}")
return subscription
@classmethod
def _init_tenant_users(cls, registration: TenantRegistration):
"""
Создание пользователей тенанта: владелец + техподдержка платформы.
ВАЖНО: CustomUser в TENANT_APPS - создаётся в схеме тенанта.
Вызывается в контексте tenant schema (connection.set_tenant уже выполнен).
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, # Пароль будет установлен через ссылку
)
owner.is_email_confirmed = True
owner.email_confirmed_at = timezone.now()
owner.is_active = False # Неактивен до установки пароля
owner.save()
logger.info(f"Аккаунт владельца создан: {owner.id}")
# Назначаем роль owner
cls._assign_owner_role(owner)
else:
logger.warning(f"Пользователь {registration.owner_email} уже существует")
# 2. Техподдержка платформы
# SECURITY: НЕ создаём отдельную учетку CustomUser для саппорта
# PlatformAdmin с is_superuser=True уже имеет полный доступ к tenant
# через PlatformAdminBackend (может залогиниться и зайти в /admin/)
logger.info("Техподдержка: используется PlatformAdmin (отдельный CustomUser НЕ создаётся)")
@classmethod
def _assign_owner_role(cls, owner):
"""Назначение роли owner владельцу тенанта."""
try:
from user_roles.services import RoleService
from user_roles.models import Role
RoleService.create_default_roles()
RoleService.assign_role_to_user(owner, Role.OWNER, created_by=None)
logger.info(f"Роль owner назначена: {owner.email}")
except Exception as e:
logger.error(f"Ошибка при назначении роли: {e}", exc_info=True)
@classmethod
def _init_tenant_data(cls):
"""Инициализация системных данных тенанта."""
try:
cls.init_tenant_data(reset=False)
except Exception as e:
logger.error(f"Ошибка при инициализации данных: {e}", exc_info=True)
# Не прерываем процесс
@classmethod
def _send_welcome_email(cls, registration: TenantRegistration):
"""Отправка приветственного письма владельцу."""
try:
from tenants.services.email import send_password_setup_email
send_password_setup_email(registration)
logger.info(f"Письмо отправлено: {registration.owner_email}")
except Exception as e:
logger.error(f"Ошибка при отправке письма: {e}", exc_info=True)
# Не прерываем процесс
@classmethod
def _finalize_registration(cls, registration: TenantRegistration, client: Client, admin_user):
"""Финализация заявки на регистрацию."""
registration.status = TenantRegistration.STATUS_APPROVED
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
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} финализирована")