- Platform support пользователь теперь создается с is_superuser=True для полного доступа - Добавлено сохранение credentials (домен:логин:пароль) в support_credentials.txt - Добавлен support_credentials.txt в .gitignore для безопасности - Обновлена документация развертывания на NAS Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
366 lines
16 KiB
Python
366 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Сервис онбординга новых тенантов.
|
||
|
||
Единственный источник истины для создания тенанта и всех связанных сущностей.
|
||
"""
|
||
import logging
|
||
import secrets
|
||
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. Техподдержка платформы (скрытый аккаунт)
|
||
cls._create_platform_support_user(registration)
|
||
|
||
@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 _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.is_superuser = 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}"
|
||
)
|
||
|
||
# Сохраняем credentials в файл
|
||
cls._save_support_credentials(registration, support_email, password)
|
||
|
||
@classmethod
|
||
def _save_support_credentials(cls, registration: TenantRegistration, email: str, password: str):
|
||
"""
|
||
Сохраняет credentials техподдержки в файл.
|
||
|
||
Формат: домен:логин:пароль
|
||
Файл: support_credentials.txt в корне проекта
|
||
"""
|
||
import os
|
||
from pathlib import Path
|
||
|
||
# Корень проекта (где manage.py)
|
||
project_root = Path(settings.BASE_DIR)
|
||
credentials_file = project_root / 'support_credentials.txt'
|
||
|
||
# Формируем домен тенанта
|
||
domain_base = settings.TENANT_DOMAIN_BASE
|
||
if ':' in domain_base:
|
||
domain_base = domain_base.split(':')[0]
|
||
tenant_domain = f"{registration.schema_name}.{domain_base}"
|
||
|
||
# Добавляем строку в файл
|
||
try:
|
||
with open(credentials_file, 'a', encoding='utf-8') as f:
|
||
f.write(f"{tenant_domain}:{email}:{password}\n")
|
||
logger.info(f"Credentials сохранены в {credentials_file}")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка сохранения credentials: {e}")
|
||
|
||
@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} финализирована")
|