feat: Version 1.10.0 - Refactorizacion, optimizacion UI y mejoras de seguridad
- Extraccion de helpers en backend: audit_helpers.py, helpers.py - Modularizacion de schemas en archivos individuales por dominio - Reduccion de audit.py en 953 lineas (74% del archivo) - Reduccion de tickets.py en 655 lineas (60% del archivo) - Expansion de auth.py con recuperacion de contrasenia y tokens - Nuevos modulos: core/email.py, core/cache.py - Reorganizacion de scripts a backend/scripts/ - Frontend: refactorizacion de audit page con array-driven components - Frontend: correccion de 11 errores ortograficos en tickets page - Frontend: proxy Docker corregido en vite.config.js - Frontend: nuevas rutas forgot-password, reset-password, organization, profile - Nuevas utilidades TS: colorUtils.ts, dateFormats.ts - 5 nuevos archivos de tests unitarios en backend/tests/unit/ - Eliminacion de 3 scripts temporales de prueba - Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
This commit is contained in:
114
backend/app/api/v1/helpers.py
Normal file
114
backend/app/api/v1/helpers.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Helper functions for API endpoints
|
||||
"""
|
||||
import uuid
|
||||
from typing import Any, Type
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Query
|
||||
from datetime import datetime, timedelta
|
||||
from app.models.user import User
|
||||
from app.models.ticket import Ticket
|
||||
from app.models.category import Category
|
||||
from app.services.audit_service import AuditService
|
||||
|
||||
|
||||
def validate_uuid_param(value: str, param_name: str = "ID") -> uuid.UUID:
|
||||
"""Valida y convierte string a UUID"""
|
||||
try:
|
||||
return uuid.UUID(value)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid {param_name} format"
|
||||
)
|
||||
|
||||
|
||||
def apply_client_permissions(query: Query, model: Type, current_user: User) -> Query:
|
||||
"""Aplica filtros de tenant y permisos de cliente"""
|
||||
query = query.where(model.tenant_id == current_user.tenant_id)
|
||||
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
||||
query = query.where(model.created_by == current_user.id)
|
||||
return query
|
||||
|
||||
|
||||
def apply_enum_filter(query: Query, model_field: Any, filter_value: str,
|
||||
enum_class: Type, filter_name: str) -> Query:
|
||||
"""Aplica filtro de enum genérico"""
|
||||
if filter_value:
|
||||
try:
|
||||
enum_val = enum_class[filter_value.upper()]
|
||||
return query.where(model_field == enum_val)
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid {filter_name}: {filter_value}"
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
async def safe_audit_log(db: AsyncSession, **kwargs):
|
||||
"""Registra en auditoría sin fallar la operación principal"""
|
||||
try:
|
||||
await AuditService.log(db=db, **kwargs)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass # Silent fail para audit logs
|
||||
|
||||
|
||||
async def generate_next_ticket_number(db: AsyncSession, tenant_id: uuid.UUID) -> str:
|
||||
"""Genera el siguiente número de ticket único para el tenant"""
|
||||
result = await db.execute(
|
||||
select(Ticket.ticket_number)
|
||||
.where(Ticket.tenant_id == tenant_id)
|
||||
.order_by(Ticket.ticket_number.desc())
|
||||
.limit(1)
|
||||
)
|
||||
last_ticket_number = result.scalar_one_or_none()
|
||||
|
||||
if last_ticket_number:
|
||||
last_number = int(last_ticket_number.split('-')[1])
|
||||
next_number = last_number + 1
|
||||
else:
|
||||
next_number = 1
|
||||
|
||||
return f"TK-{next_number:06d}"
|
||||
|
||||
|
||||
def calculate_sla_deadlines(category: Category = None) -> tuple[datetime, datetime]:
|
||||
"""Calcula SLA response y resolution deadlines"""
|
||||
if not category:
|
||||
return None, None
|
||||
|
||||
now = datetime.utcnow()
|
||||
sla_response_due = now + timedelta(hours=category.sla_response_hours)
|
||||
sla_resolution_due = now + timedelta(hours=category.sla_resolution_hours)
|
||||
return sla_response_due, sla_resolution_due
|
||||
|
||||
|
||||
def ticket_to_dict(ticket: Ticket) -> dict:
|
||||
"""Convierte un modelo Ticket a diccionario de respuesta"""
|
||||
return {
|
||||
"id": str(ticket.id),
|
||||
"ticket_number": ticket.ticket_number,
|
||||
"subject": ticket.subject,
|
||||
"title": ticket.subject,
|
||||
"description": ticket.description,
|
||||
"status": ticket.status.value,
|
||||
"priority": ticket.priority.value,
|
||||
"category_id": str(ticket.category_id) if ticket.category_id else None,
|
||||
"category_name": ticket.category.name if ticket.category else None,
|
||||
"affected_system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None,
|
||||
"affected_system_name": ticket.affected_system.name if ticket.affected_system else None,
|
||||
"created_by": str(ticket.created_by),
|
||||
"assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None,
|
||||
"assigned_to_name": f"{ticket.assigned_to_user.first_name} {ticket.assigned_to_user.last_name}" if ticket.assigned_to_user else None,
|
||||
"created_at": ticket.created_at,
|
||||
"updated_at": ticket.updated_at,
|
||||
"sla_response_due": ticket.sla_response_due,
|
||||
"sla_resolution_due": ticket.sla_resolution_due,
|
||||
"first_response_at": ticket.first_response_at,
|
||||
"resolved_at": ticket.resolved_at,
|
||||
"tenant_id": str(ticket.tenant_id)
|
||||
}
|
||||
Reference in New Issue
Block a user