Files
plantillas-proyectos/backend/api/v1/modules/a76/tenants/service.py
acazares 52b8fcd434 feat: Implement multi-tenancy support in middleware and security layers
- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
2025-11-11 14:15:31 -06:00

212 lines
6.7 KiB
Python

"""
Capa de servicio para lógica de negocio de tenants
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from typing import List, Optional
import json
import logging
from .models import Tenant, TenantType
from .dto import TenantCreateDTO, TenantUpdateDTO, TenantResponseDTO
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 == True)
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")