Files
saas_backend/app/services/auth/tenant_service.py
T

307 lines
12 KiB
Python

from sqlalchemy.orm import Session
from sqlalchemy import or_, cast, String
from fastapi import HTTPException, status
from app.models.auth.tenant_model import Tenant
from app.models.auth.tenant_module_model import TenantModule
from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantPaginatedResponse, TenantResponse
import uuid
from typing import Optional
from app.services.auth.event_service import EventService
import logging
logger = logging.getLogger(__name__)
class TenantService:
@staticmethod
def create_tenant(db: Session, tenant_data: TenantCreate) -> Tenant:
existing = db.query(Tenant).filter(Tenant.tenant_name == tenant_data.tenant_name).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant name already exists"
)
existing = db.query(Tenant).filter(Tenant.tenant_domain == tenant_data.tenant_domain).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant domain already exists"
)
provisioning_id = str(uuid.uuid4())
try:
# 1. Create Tenant
tenant = Tenant(
tenant_name=tenant_data.tenant_name,
tenant_domain=tenant_data.tenant_domain,
tenant_logo_url=tenant_data.tenant_logo_url
)
db.add(tenant)
db.flush()
# 2. Create Tenant Modules & Build Event Targets
event_targets = []
if tenant_data.modules:
for mod_data in tenant_data.modules:
tm = TenantModule(
tenant_id=tenant.id,
module_id=mod_data.module_id,
assigned_environment_slug=mod_data.environment_slug,
is_active=True
)
db.add(tm)
event_targets.append({
"module_id": str(mod_data.module_id),
"environment_slug": mod_data.environment_slug
})
if event_targets:
logger.info(f"Creating tenant {tenant.tenant_name}. Processing {len(event_targets)} event targets.")
payload = {
"tenant_id": str(tenant.id),
"tenant_name": tenant.tenant_name,
"tenant_domain": tenant.tenant_domain,
"tenant_logo_url": tenant.tenant_logo_url,
"provisioning_id": provisioning_id,
"targets": event_targets
}
EventService.emit_event(
db,
event_type="TENANT_PROVISION_REQUESTED",
payload=payload,
tenant_id=tenant.id
)
logger.info("Event TENANT_PROVISION_REQUESTED emitted to outbox.")
db.commit()
db.refresh(tenant)
return tenant
except Exception as e:
db.rollback()
raise e
@staticmethod
def get_all_tenants(db: Session):
return db.query(Tenant).all()
@staticmethod
def get_tenant_by_id(db: Session, tenant_id: uuid.UUID) -> Tenant:
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
return tenant
@staticmethod
def update_tenant(db: Session, tenant_id: uuid.UUID, tenant_data: TenantUpdate) -> Tenant:
tenant = TenantService.get_tenant_by_id(db, tenant_id)
update_dict = tenant_data.model_dump(exclude_unset=True)
should_emit_update = False
should_emit_status = False
if "tenant_name" in update_dict and update_dict["tenant_name"] != tenant.tenant_name:
existing = db.query(Tenant).filter(Tenant.tenant_name == update_dict["tenant_name"]).first()
if existing:
raise HTTPException(status_code=400, detail="Tenant name already exists")
should_emit_update = True
if "tenant_domain" in update_dict and update_dict["tenant_domain"] != tenant.tenant_domain:
existing = db.query(Tenant).filter(Tenant.tenant_domain == update_dict["tenant_domain"]).first()
if existing:
raise HTTPException(status_code=400, detail="Tenant domain already exists")
should_emit_update = True
if "tenant_logo_url" in update_dict and update_dict["tenant_logo_url"] != tenant.tenant_logo_url:
should_emit_update = True
if "is_active" in update_dict and update_dict["is_active"] != tenant.is_active:
should_emit_status = True
if "modules" in update_dict:
modules_data = update_dict.pop("modules")
if modules_data is not None:
current_modules = db.query(TenantModule).filter(TenantModule.tenant_id == tenant.id).all()
current_map = {tm.module_id: tm for tm in current_modules}
new_map = {m["module_id"]: m for m in modules_data}
event_targets = []
provisioning_id = str(uuid.uuid4())
for module_id, data in new_map.items():
new_env_slug = data.get("environment_slug")
if module_id in current_map:
tm = current_map[module_id]
if tm.assigned_environment_slug != new_env_slug or not tm.is_active:
tm.assigned_environment_slug = new_env_slug
tm.is_active = True
event_targets.append({
"module_id": str(module_id),
"environment_slug": new_env_slug
})
else:
tm = TenantModule(
tenant_id=tenant.id,
module_id=module_id,
assigned_environment_slug=new_env_slug,
is_active=True
)
db.add(tm)
event_targets.append({
"module_id": str(module_id),
"environment_slug": new_env_slug
})
for module_id, tm in current_map.items():
if module_id not in new_map:
tm.is_active = False
db.flush()
if event_targets:
payload = {
"tenant_id": str(tenant.id),
"tenant_name": tenant.tenant_name,
"provisioning_id": provisioning_id,
"targets": event_targets
}
EventService.emit_event(
db,
event_type="TENANT_PROVISION_REQUESTED",
payload=payload,
tenant_id=tenant.id
)
for key, value in update_dict.items():
setattr(tenant, key, value)
if should_emit_update or should_emit_status:
active_modules = db.query(TenantModule).filter(
TenantModule.tenant_id == tenant.id,
TenantModule.is_active == True
).all()
broadcast_targets = [
{"module_id": str(tm.module_id), "environment_slug": tm.assigned_environment_slug or "prod"}
for tm in active_modules
]
if broadcast_targets:
if should_emit_update:
payload = {
"tenant_id": str(tenant.id),
"tenant_name": tenant.tenant_name,
"tenant_domain": tenant.tenant_domain,
"tenant_logo_url": tenant.tenant_logo_url,
"targets": broadcast_targets
}
EventService.emit_event(
db,
event_type="TENANT_UPDATED",
payload=payload,
tenant_id=tenant.id
)
if should_emit_status:
payload = {
"tenant_id": str(tenant.id),
"is_active": tenant.is_active,
"status": "ACTIVE" if tenant.is_active else "INACTIVE",
"targets": broadcast_targets
}
EventService.emit_event(
db,
event_type="TENANT_STATUS_CHANGED",
payload=payload,
tenant_id=tenant.id
)
db.commit()
db.refresh(tenant)
return tenant
@staticmethod
def delete_tenant(db: Session, tenant_id: uuid.UUID):
tenant = TenantService.get_tenant_by_id(db, tenant_id)
active_modules = db.query(TenantModule).filter(
TenantModule.tenant_id == tenant.id,
TenantModule.is_active == True
).all()
if active_modules:
broadcast_targets = [
{"module_id": str(tm.module_id), "environment_slug": tm.assigned_environment_slug or "prod"}
for tm in active_modules
]
if broadcast_targets:
payload = {
"tenant_id": str(tenant.id),
"tenant_name": tenant.tenant_name,
"targets": broadcast_targets
}
EventService.emit_event(
db,
event_type="TENANT_DEPROVISION_REQUESTED",
payload=payload,
tenant_id=tenant.id
)
db.delete(tenant)
db.commit()
return {"message": "Tenant deleted successfully"}
@staticmethod
def get_tenants_paginated(
db: Session,
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
is_active: Optional[bool] = None,
) -> TenantPaginatedResponse:
query = db.query(Tenant)
if search and search.strip():
search_term = search.strip()
query = query.filter(
or_(
Tenant.tenant_name.ilike(f"%{search_term}%"),
Tenant.tenant_domain.ilike(f"%{search_term}%"),
cast(Tenant.id, String).ilike(f"%{search_term}%"),
)
)
if is_active is not None:
query = query.filter(Tenant.is_active == is_active)
total = query.count()
offset = (page - 1) * page_size
tenants = query.offset(offset).limit(page_size).all()
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
return TenantPaginatedResponse(
items=[TenantResponse.from_orm(tenant) for tenant in tenants],
total=total,
page=page,
page_size=page_size,
total_pages=total_pages,
)