Improve teh securty of teh DB

This commit is contained in:
Urtzi Alfaro
2025-10-19 19:22:37 +02:00
parent 62971c07d7
commit 05da20357d
87 changed files with 7998 additions and 932 deletions

View File

@@ -0,0 +1,229 @@
# services/production/app/api/equipment.py
"""
Equipment API - CRUD operations on Equipment model
"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from typing import Optional
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
from app.core.database import get_db
from app.services.production_service import ProductionService
from app.schemas.equipment import (
EquipmentCreate,
EquipmentUpdate,
EquipmentResponse,
EquipmentListResponse
)
from app.models.production import EquipmentStatus, EquipmentType
from app.core.config import settings
logger = structlog.get_logger()
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-equipment"])
# Initialize audit logger
audit_logger = create_audit_logger("production-service")
def get_production_service() -> ProductionService:
"""Dependency injection for production service"""
from app.core.database import database_manager
return ProductionService(database_manager, settings)
@router.get(
route_builder.build_base_route("equipment"),
response_model=EquipmentListResponse
)
async def list_equipment(
tenant_id: UUID = Path(...),
status: Optional[EquipmentStatus] = Query(None, description="Filter by status"),
type: Optional[EquipmentType] = Query(None, description="Filter by equipment type"),
is_active: Optional[bool] = Query(None, description="Filter by active status"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=100, description="Page size"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""List equipment with filters: status, type, active status"""
try:
filters = {
"status": status,
"type": type,
"is_active": is_active
}
equipment_list = await production_service.get_equipment_list(tenant_id, filters, page, page_size)
logger.info("Retrieved equipment list",
tenant_id=str(tenant_id), filters=filters)
return equipment_list
except Exception as e:
logger.error("Error listing equipment",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to list equipment")
@router.post(
route_builder.build_base_route("equipment"),
response_model=EquipmentResponse
)
async def create_equipment(
equipment_data: EquipmentCreate,
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Create a new equipment item"""
try:
equipment = await production_service.create_equipment(tenant_id, equipment_data)
logger.info("Created equipment",
equipment_id=str(equipment.id), tenant_id=str(tenant_id))
# Audit log
await audit_logger.log(
action=AuditAction.CREATE,
resource_type="equipment",
resource_id=str(equipment.id),
user_id=current_user.get('user_id'),
tenant_id=str(tenant_id),
severity=AuditSeverity.INFO,
details={"equipment_name": equipment.name, "equipment_type": equipment.type.value}
)
return EquipmentResponse.model_validate(equipment)
except ValueError as e:
logger.warning("Validation error creating equipment",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating equipment",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to create equipment")
@router.get(
route_builder.build_base_route("equipment/{equipment_id}"),
response_model=EquipmentResponse
)
async def get_equipment(
tenant_id: UUID = Path(...),
equipment_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get a specific equipment item"""
try:
equipment = await production_service.get_equipment(tenant_id, equipment_id)
if not equipment:
raise HTTPException(status_code=404, detail="Equipment not found")
logger.info("Retrieved equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
return EquipmentResponse.model_validate(equipment)
except HTTPException:
raise
except Exception as e:
logger.error("Error retrieving equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to retrieve equipment")
@router.put(
route_builder.build_base_route("equipment/{equipment_id}"),
response_model=EquipmentResponse
)
async def update_equipment(
equipment_data: EquipmentUpdate,
tenant_id: UUID = Path(...),
equipment_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Update an equipment item"""
try:
equipment = await production_service.update_equipment(tenant_id, equipment_id, equipment_data)
if not equipment:
raise HTTPException(status_code=404, detail="Equipment not found")
logger.info("Updated equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
# Audit log
await audit_logger.log(
action=AuditAction.UPDATE,
resource_type="equipment",
resource_id=str(equipment_id),
user_id=current_user.get('user_id'),
tenant_id=str(tenant_id),
severity=AuditSeverity.INFO,
details={"updates": equipment_data.model_dump(exclude_unset=True)}
)
return EquipmentResponse.model_validate(equipment)
except HTTPException:
raise
except ValueError as e:
logger.warning("Validation error updating equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to update equipment")
@router.delete(
route_builder.build_base_route("equipment/{equipment_id}")
)
async def delete_equipment(
tenant_id: UUID = Path(...),
equipment_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Delete (soft delete) an equipment item"""
try:
success = await production_service.delete_equipment(tenant_id, equipment_id)
if not success:
raise HTTPException(status_code=404, detail="Equipment not found")
logger.info("Deleted equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
# Audit log
await audit_logger.log(
action=AuditAction.DELETE,
resource_type="equipment",
resource_id=str(equipment_id),
user_id=current_user.get('user_id'),
tenant_id=str(tenant_id),
severity=AuditSeverity.WARNING,
details={"action": "soft_delete"}
)
return {"message": "Equipment deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error("Error deleting equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to delete equipment")

View File

@@ -21,6 +21,7 @@ from app.models.production import (
)
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
from shared.utils.alert_generator import generate_equipment_alerts
from shared.messaging.rabbitmq import RabbitMQClient
logger = structlog.get_logger()
router = APIRouter(prefix="/internal/demo", tags=["internal"])
@@ -432,14 +433,39 @@ async def clone_demo_data(
# Commit cloned data first
await db.commit()
# Generate equipment maintenance and status alerts
# Generate equipment maintenance and status alerts with RabbitMQ publishing
rabbitmq_client = None
try:
alerts_count = await generate_equipment_alerts(db, virtual_uuid, session_time)
# Initialize RabbitMQ client for alert publishing
rabbitmq_host = os.getenv("RABBITMQ_HOST", "rabbitmq-service")
rabbitmq_user = os.getenv("RABBITMQ_USER", "bakery")
rabbitmq_password = os.getenv("RABBITMQ_PASSWORD", "forecast123")
rabbitmq_port = os.getenv("RABBITMQ_PORT", "5672")
rabbitmq_vhost = os.getenv("RABBITMQ_VHOST", "/")
rabbitmq_url = f"amqp://{rabbitmq_user}:{rabbitmq_password}@{rabbitmq_host}:{rabbitmq_port}{rabbitmq_vhost}"
rabbitmq_client = RabbitMQClient(rabbitmq_url, service_name="production")
await rabbitmq_client.connect()
# Generate alerts and publish to RabbitMQ
alerts_count = await generate_equipment_alerts(
db,
virtual_uuid,
session_time,
rabbitmq_client=rabbitmq_client
)
stats["alerts_generated"] += alerts_count
await db.commit()
logger.info(f"Generated {alerts_count} equipment alerts")
except Exception as alert_error:
logger.warning(f"Alert generation failed: {alert_error}", exc_info=True)
finally:
# Clean up RabbitMQ connection
if rabbitmq_client:
try:
await rabbitmq_client.disconnect()
except Exception as cleanup_error:
logger.warning(f"Error disconnecting RabbitMQ: {cleanup_error}")
total_records = sum(stats.values())
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)

View File

@@ -23,6 +23,7 @@ from app.api import (
production_dashboard,
analytics,
quality_templates,
equipment,
internal_demo
)
@@ -166,6 +167,7 @@ service.setup_custom_middleware()
# Include standardized routers
# NOTE: Register more specific routes before generic parameterized routes
service.add_router(quality_templates.router) # Register first to avoid route conflicts
service.add_router(equipment.router)
service.add_router(production_batches.router)
service.add_router(production_schedules.router)
service.add_router(production_operations.router)

View File

@@ -0,0 +1,152 @@
"""
Equipment Repository
"""
from typing import Optional, List, Dict, Any
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from uuid import UUID
import structlog
from app.repositories.base import ProductionBaseRepository
from app.models.production import Equipment, EquipmentStatus, EquipmentType
logger = structlog.get_logger()
class EquipmentRepository(ProductionBaseRepository):
"""Repository for equipment operations"""
def __init__(self, session: AsyncSession):
super().__init__(Equipment, session)
async def get_equipment_filtered(
self,
filters: Dict[str, Any],
page: int = 1,
page_size: int = 50
) -> List[Equipment]:
"""Get equipment list with filters and pagination"""
try:
# Build base query
query = select(Equipment).filter(Equipment.tenant_id == UUID(filters.get("tenant_id")))
# Apply status filter
if "status" in filters and filters["status"]:
query = query.filter(Equipment.status == filters["status"])
# Apply type filter
if "type" in filters and filters["type"]:
query = query.filter(Equipment.type == filters["type"])
# Apply active filter
if "is_active" in filters and filters["is_active"] is not None:
query = query.filter(Equipment.is_active == filters["is_active"])
# Apply pagination
query = query.order_by(Equipment.created_at.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await self.session.execute(query)
return list(result.scalars().all())
except Exception as e:
logger.error("Error getting filtered equipment", error=str(e), filters=filters)
raise
async def count_equipment_filtered(self, filters: Dict[str, Any]) -> int:
"""Count equipment matching filters"""
try:
# Build base query
query = select(func.count(Equipment.id)).filter(
Equipment.tenant_id == UUID(filters.get("tenant_id"))
)
# Apply status filter
if "status" in filters and filters["status"]:
query = query.filter(Equipment.status == filters["status"])
# Apply type filter
if "type" in filters and filters["type"]:
query = query.filter(Equipment.type == filters["type"])
# Apply active filter
if "is_active" in filters and filters["is_active"] is not None:
query = query.filter(Equipment.is_active == filters["is_active"])
result = await self.session.execute(query)
return result.scalar() or 0
except Exception as e:
logger.error("Error counting filtered equipment", error=str(e), filters=filters)
raise
async def get_equipment_by_id(self, tenant_id: UUID, equipment_id: UUID) -> Optional[Equipment]:
"""Get equipment by ID and tenant"""
try:
query = select(Equipment).filter(
and_(
Equipment.id == equipment_id,
Equipment.tenant_id == tenant_id
)
)
result = await self.session.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting equipment by ID",
error=str(e),
equipment_id=str(equipment_id),
tenant_id=str(tenant_id))
raise
async def create_equipment(self, equipment_data: Dict[str, Any]) -> Equipment:
"""Create new equipment"""
try:
equipment = Equipment(**equipment_data)
self.session.add(equipment)
await self.session.flush()
await self.session.refresh(equipment)
return equipment
except Exception as e:
logger.error("Error creating equipment", error=str(e), data=equipment_data)
raise
async def update_equipment(
self,
equipment_id: UUID,
updates: Dict[str, Any]
) -> Optional[Equipment]:
"""Update equipment"""
try:
equipment = await self.get(equipment_id)
if not equipment:
return None
for key, value in updates.items():
if hasattr(equipment, key) and value is not None:
setattr(equipment, key, value)
await self.session.flush()
await self.session.refresh(equipment)
return equipment
except Exception as e:
logger.error("Error updating equipment", error=str(e), equipment_id=str(equipment_id))
raise
async def delete_equipment(self, equipment_id: UUID) -> bool:
"""Soft delete equipment (set is_active to False)"""
try:
equipment = await self.get(equipment_id)
if not equipment:
return False
equipment.is_active = False
await self.session.flush()
return True
except Exception as e:
logger.error("Error deleting equipment", error=str(e), equipment_id=str(equipment_id))
raise

View File

@@ -0,0 +1,171 @@
# services/production/app/schemas/equipment.py
"""
Equipment schemas for Production Service
"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
from datetime import datetime
from uuid import UUID
from app.models.production import EquipmentType, EquipmentStatus
class EquipmentCreate(BaseModel):
"""Schema for creating new equipment"""
name: str = Field(..., min_length=1, max_length=255, description="Equipment name")
type: EquipmentType = Field(..., description="Equipment type")
model: Optional[str] = Field(None, max_length=100, description="Equipment model")
serial_number: Optional[str] = Field(None, max_length=100, description="Serial number")
location: Optional[str] = Field(None, max_length=255, description="Physical location")
status: EquipmentStatus = Field(default=EquipmentStatus.OPERATIONAL, description="Equipment status")
# Installation and maintenance
install_date: Optional[datetime] = Field(None, description="Installation date")
last_maintenance_date: Optional[datetime] = Field(None, description="Last maintenance date")
next_maintenance_date: Optional[datetime] = Field(None, description="Next scheduled maintenance date")
maintenance_interval_days: Optional[int] = Field(None, ge=1, description="Maintenance interval in days")
# Performance metrics
efficiency_percentage: Optional[float] = Field(None, ge=0, le=100, description="Current efficiency percentage")
uptime_percentage: Optional[float] = Field(None, ge=0, le=100, description="Overall uptime percentage")
energy_usage_kwh: Optional[float] = Field(None, ge=0, description="Current energy usage in kWh")
# Specifications
power_kw: Optional[float] = Field(None, ge=0, description="Power consumption in kilowatts")
capacity: Optional[float] = Field(None, ge=0, description="Equipment capacity")
weight_kg: Optional[float] = Field(None, ge=0, description="Weight in kilograms")
# Temperature monitoring
current_temperature: Optional[float] = Field(None, description="Current temperature")
target_temperature: Optional[float] = Field(None, description="Target temperature")
# Notes
notes: Optional[str] = Field(None, description="Additional notes")
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "Horno Principal #1",
"type": "oven",
"model": "Miwe Condo CO 4.1212",
"serial_number": "MCO-2021-001",
"location": "Área de Horneado - Zona A",
"status": "operational",
"install_date": "2021-03-15T00:00:00Z",
"maintenance_interval_days": 90,
"efficiency_percentage": 92.0,
"uptime_percentage": 98.5,
"power_kw": 45.0,
"capacity": 24.0
}
}
)
class EquipmentUpdate(BaseModel):
"""Schema for updating equipment"""
name: Optional[str] = Field(None, min_length=1, max_length=255)
type: Optional[EquipmentType] = None
model: Optional[str] = Field(None, max_length=100)
serial_number: Optional[str] = Field(None, max_length=100)
location: Optional[str] = Field(None, max_length=255)
status: Optional[EquipmentStatus] = None
# Installation and maintenance
install_date: Optional[datetime] = None
last_maintenance_date: Optional[datetime] = None
next_maintenance_date: Optional[datetime] = None
maintenance_interval_days: Optional[int] = Field(None, ge=1)
# Performance metrics
efficiency_percentage: Optional[float] = Field(None, ge=0, le=100)
uptime_percentage: Optional[float] = Field(None, ge=0, le=100)
energy_usage_kwh: Optional[float] = Field(None, ge=0)
# Specifications
power_kw: Optional[float] = Field(None, ge=0)
capacity: Optional[float] = Field(None, ge=0)
weight_kg: Optional[float] = Field(None, ge=0)
# Temperature monitoring
current_temperature: Optional[float] = None
target_temperature: Optional[float] = None
# Notes
notes: Optional[str] = None
# Status flag
is_active: Optional[bool] = None
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "maintenance",
"last_maintenance_date": "2024-01-15T00:00:00Z",
"next_maintenance_date": "2024-04-15T00:00:00Z",
"efficiency_percentage": 88.0
}
}
)
class EquipmentResponse(BaseModel):
"""Schema for equipment response"""
id: UUID
tenant_id: UUID
name: str
type: EquipmentType
model: Optional[str] = None
serial_number: Optional[str] = None
location: Optional[str] = None
status: EquipmentStatus
# Installation and maintenance
install_date: Optional[datetime] = None
last_maintenance_date: Optional[datetime] = None
next_maintenance_date: Optional[datetime] = None
maintenance_interval_days: Optional[int] = None
# Performance metrics
efficiency_percentage: Optional[float] = None
uptime_percentage: Optional[float] = None
energy_usage_kwh: Optional[float] = None
# Specifications
power_kw: Optional[float] = None
capacity: Optional[float] = None
weight_kg: Optional[float] = None
# Temperature monitoring
current_temperature: Optional[float] = None
target_temperature: Optional[float] = None
# Status
is_active: bool
notes: Optional[str] = None
# Timestamps
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class EquipmentListResponse(BaseModel):
"""Schema for paginated equipment list response"""
equipment: List[EquipmentResponse]
total_count: int
page: int
page_size: int
model_config = ConfigDict(
json_schema_extra={
"example": {
"equipment": [],
"total_count": 10,
"page": 1,
"page_size": 50
}
}
)

View File

@@ -1386,4 +1386,146 @@ class ProductionService:
except Exception as e:
logger.error("Error getting batch with transformations",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
return {}
return {}
# ================================================================
# EQUIPMENT MANAGEMENT METHODS
# ================================================================
async def get_equipment_list(
self,
tenant_id: UUID,
filters: Dict[str, Any],
page: int = 1,
page_size: int = 50
) -> Dict[str, Any]:
"""Get list of equipment with filtering and pagination"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.equipment_repository import EquipmentRepository
equipment_repo = EquipmentRepository(session)
# Apply filters
filter_dict = {k: v for k, v in filters.items() if v is not None}
filter_dict["tenant_id"] = str(tenant_id)
# Get equipment with pagination
equipment_list = await equipment_repo.get_equipment_filtered(filter_dict, page, page_size)
total_count = await equipment_repo.count_equipment_filtered(filter_dict)
# Convert to response format
from app.schemas.equipment import EquipmentResponse
equipment_responses = [
EquipmentResponse.model_validate(eq) for eq in equipment_list
]
return {
"equipment": equipment_responses,
"total_count": total_count,
"page": page,
"page_size": page_size
}
except Exception as e:
logger.error("Error getting equipment list",
error=str(e), tenant_id=str(tenant_id))
raise
async def get_equipment(self, tenant_id: UUID, equipment_id: UUID):
"""Get a specific equipment item"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.equipment_repository import EquipmentRepository
equipment_repo = EquipmentRepository(session)
equipment = await equipment_repo.get_equipment_by_id(tenant_id, equipment_id)
if not equipment:
return None
logger.info("Retrieved equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
return equipment
except Exception as e:
logger.error("Error getting equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise
async def create_equipment(self, tenant_id: UUID, equipment_data):
"""Create a new equipment item"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.equipment_repository import EquipmentRepository
equipment_repo = EquipmentRepository(session)
# Prepare equipment data
equipment_dict = equipment_data.model_dump()
equipment_dict["tenant_id"] = tenant_id
# Create equipment
equipment = await equipment_repo.create_equipment(equipment_dict)
logger.info("Created equipment",
equipment_id=str(equipment.id), tenant_id=str(tenant_id))
return equipment
except Exception as e:
logger.error("Error creating equipment",
error=str(e), tenant_id=str(tenant_id))
raise
async def update_equipment(self, tenant_id: UUID, equipment_id: UUID, equipment_update):
"""Update an equipment item"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.equipment_repository import EquipmentRepository
equipment_repo = EquipmentRepository(session)
# First verify equipment belongs to tenant
equipment = await equipment_repo.get_equipment_by_id(tenant_id, equipment_id)
if not equipment:
return None
# Update equipment
updated_equipment = await equipment_repo.update_equipment(
equipment_id,
equipment_update.model_dump(exclude_none=True)
)
logger.info("Updated equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
return updated_equipment
except Exception as e:
logger.error("Error updating equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise
async def delete_equipment(self, tenant_id: UUID, equipment_id: UUID) -> bool:
"""Delete (soft delete) an equipment item"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.equipment_repository import EquipmentRepository
equipment_repo = EquipmentRepository(session)
# First verify equipment belongs to tenant
equipment = await equipment_repo.get_equipment_by_id(tenant_id, equipment_id)
if not equipment:
return False
# Soft delete equipment
success = await equipment_repo.delete_equipment(equipment_id)
logger.info("Deleted equipment",
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
return success
except Exception as e:
logger.error("Error deleting equipment",
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
raise