- Rearranged imports in multiple files for consistency and clarity. - Updated logging middleware to exclude specific paths from logging. - Enhanced security module by cleaning up token handling and improving tenant validation. - Added tenant and company scoped mixins for better database model management. - Implemented generic CRUD routes for tenant-scoped resources. - Improved error handling and response management in API routes. - Cleaned up login and logout processes to ensure proper session management. - Introduced mechanisms to clear local storage and cookies on tenant change. - Enhanced company store to detect tenant changes and clear data accordingly. - Added new DTO mixins for currency and value affect flags.
213 lines
6.7 KiB
Python
213 lines
6.7 KiB
Python
"""
|
|
Capa de servicio para lógica de negocio de tenants
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .dto import TenantCreateDTO, TenantResponseDTO, TenantUpdateDTO
|
|
from .models import Tenant, TenantType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TenantService:
|
|
"""Servicio para gestión de tenants"""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
def create_tenant(self, tenant_data: TenantCreateDTO) -> TenantResponseDTO:
|
|
"""
|
|
Crea un nuevo tenant en el sistema
|
|
|
|
Args:
|
|
tenant_data: Datos del tenant a crear
|
|
|
|
Returns:
|
|
TenantResponseDTO con información del tenant creado
|
|
|
|
Raises:
|
|
HTTPException: Si el slug o realm ya existen
|
|
"""
|
|
try:
|
|
# Verificar que no exista el slug
|
|
existing = (
|
|
self.db.query(Tenant).filter(Tenant.slug == tenant_data.slug).first()
|
|
)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Tenant with slug '{tenant_data.slug}' already exists",
|
|
)
|
|
|
|
# Crear tenant
|
|
db_tenant = Tenant(
|
|
name=tenant_data.name,
|
|
slug=tenant_data.slug,
|
|
keycloak_realm=tenant_data.keycloak_realm,
|
|
type=TenantType(tenant_data.type.value),
|
|
contact_name=tenant_data.contact_name,
|
|
contact_email=tenant_data.contact_email,
|
|
contact_phone=tenant_data.contact_phone,
|
|
is_active=True,
|
|
)
|
|
|
|
self.db.add(db_tenant)
|
|
self.db.commit()
|
|
self.db.refresh(db_tenant)
|
|
|
|
logger.info(f"Tenant created: {db_tenant.id} - {db_tenant.name}")
|
|
|
|
return TenantResponseDTO.model_validate(db_tenant)
|
|
|
|
except IntegrityError as e:
|
|
self.db.rollback()
|
|
logger.error(f"IntegrityError creating tenant: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=400, detail="Tenant with this slug or realm already exists"
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error creating tenant: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="Error creating tenant")
|
|
|
|
def get_tenant(self, tenant_id: int) -> Optional[TenantResponseDTO]:
|
|
"""
|
|
Obtiene un tenant por ID
|
|
|
|
Args:
|
|
tenant_id: ID del tenant
|
|
|
|
Returns:
|
|
TenantResponseDTO o None si no existe
|
|
"""
|
|
tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
|
if not tenant:
|
|
return None
|
|
return TenantResponseDTO.model_validate(tenant)
|
|
|
|
def get_tenant_by_slug(self, slug: str) -> Optional[TenantResponseDTO]:
|
|
"""Obtiene un tenant por slug"""
|
|
tenant = self.db.query(Tenant).filter(Tenant.slug == slug).first()
|
|
if not tenant:
|
|
return None
|
|
return TenantResponseDTO.model_validate(tenant)
|
|
|
|
def list_tenants(
|
|
self, skip: int = 0, limit: int = 100, active_only: bool = False
|
|
) -> List[TenantResponseDTO]:
|
|
"""
|
|
Lista todos los tenants
|
|
|
|
Args:
|
|
skip: Número de registros a omitir
|
|
limit: Número máximo de registros a retornar
|
|
active_only: Si True, solo retorna tenants activos
|
|
|
|
Returns:
|
|
Lista de TenantResponseDTO
|
|
"""
|
|
query = self.db.query(Tenant)
|
|
|
|
if active_only:
|
|
query = query.filter(Tenant.is_active)
|
|
|
|
tenants = query.offset(skip).limit(limit).all()
|
|
return [TenantResponseDTO.model_validate(t) for t in tenants]
|
|
|
|
def update_tenant(
|
|
self, tenant_id: int, tenant_data: TenantUpdateDTO
|
|
) -> Optional[TenantResponseDTO]:
|
|
"""
|
|
Actualiza un tenant
|
|
|
|
Args:
|
|
tenant_id: ID del tenant a actualizar
|
|
tenant_data: Datos a actualizar
|
|
|
|
Returns:
|
|
TenantResponseDTO actualizado o None si no existe
|
|
"""
|
|
tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
|
if not tenant:
|
|
return None
|
|
|
|
# Actualizar solo campos proporcionados
|
|
update_data = tenant_data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(tenant, field, value)
|
|
|
|
try:
|
|
self.db.commit()
|
|
self.db.refresh(tenant)
|
|
logger.info(f"Tenant updated: {tenant_id}")
|
|
return TenantResponseDTO.model_validate(tenant)
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error updating tenant {tenant_id}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="Error updating tenant")
|
|
|
|
def delete_tenant(self, tenant_id: int) -> bool:
|
|
"""
|
|
Elimina (desactiva) un tenant
|
|
|
|
Args:
|
|
tenant_id: ID del tenant a eliminar
|
|
|
|
Returns:
|
|
True si se eliminó, False si no existe
|
|
"""
|
|
tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
|
if not tenant:
|
|
return False
|
|
|
|
# Soft delete: marcar como inactivo
|
|
tenant.is_active = False
|
|
|
|
try:
|
|
self.db.commit()
|
|
logger.info(f"Tenant deleted (soft): {tenant_id}")
|
|
return True
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error deleting tenant {tenant_id}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="Error deleting tenant")
|
|
|
|
def upgrade_to_dedicated(
|
|
self, tenant_id: int, db_config: dict
|
|
) -> Optional[TenantResponseDTO]:
|
|
"""
|
|
Actualiza un tenant de BD compartida a BD dedicada
|
|
|
|
Args:
|
|
tenant_id: ID del tenant
|
|
db_config: Configuración de BD dedicada
|
|
|
|
Returns:
|
|
TenantResponseDTO actualizado
|
|
"""
|
|
tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
|
if not tenant:
|
|
return None
|
|
|
|
tenant.type = TenantType.DEDICATED
|
|
tenant.db_config = json.dumps(db_config)
|
|
|
|
try:
|
|
self.db.commit()
|
|
self.db.refresh(tenant)
|
|
logger.info(f"Tenant upgraded to dedicated DB: {tenant_id}")
|
|
return TenantResponseDTO.model_validate(tenant)
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error upgrading tenant {tenant_id}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="Error upgrading tenant")
|