Roles
This commit is contained in:
@@ -11,7 +11,7 @@ import uuid
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_user, get_current_tenant
|
||||
from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
||||
from app.models.user import User
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.category import Category
|
||||
from app.models.system import System
|
||||
@@ -28,98 +28,27 @@ from app.api.v1.helpers import (
|
||||
safe_audit_log, generate_next_ticket_number, calculate_sla_deadlines, ticket_to_dict
|
||||
)
|
||||
from app.services.audit_service import AuditService
|
||||
from app.services.ticket_service import TicketService, get_ticket_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=TicketResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_ticket(ticket: TicketCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
async def create_ticket(
|
||||
ticket: TicketCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
ticket_service: TicketService = Depends(get_ticket_service),
|
||||
):
|
||||
"""Crear un nuevo ticket"""
|
||||
max_retries = 3
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
ticket_number = await generate_next_ticket_number(db, current_user.tenant_id)
|
||||
category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None
|
||||
system_uuid = uuid.UUID(ticket.affected_system_id) if ticket.affected_system_id else None
|
||||
|
||||
category = None
|
||||
if category_uuid:
|
||||
category = await db.get(Category, category_uuid)
|
||||
if not category:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"La categoría con ID {ticket.category_id} no existe.")
|
||||
# ✅ SECURITY: Validate category belongs to current tenant (prevents cross-tenant category injection)
|
||||
if category.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"La categoría con ID {ticket.category_id} no existe.")
|
||||
|
||||
if system_uuid:
|
||||
system = await db.get(System, system_uuid)
|
||||
if not system:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"El sistema con ID {ticket.affected_system_id} no existe.")
|
||||
# ✅ SECURITY: Validate system belongs to current tenant (prevents cross-tenant system injection)
|
||||
if system.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"El sistema con ID {ticket.affected_system_id} no existe.")
|
||||
|
||||
sla_response_due, sla_resolution_due = calculate_sla_deadlines(category)
|
||||
assigned_to_user = category.auto_assign_to if category and category.auto_assign_to else None
|
||||
|
||||
db_ticket = Ticket(
|
||||
id=uuid.uuid4(), tenant_id=current_user.tenant_id, ticket_number=ticket_number,
|
||||
subject=ticket.subject, description=ticket.description, category_id=category_uuid,
|
||||
affected_system_id=system_uuid, priority=TicketPriority[ticket.priority.upper()],
|
||||
created_by=current_user.id, assigned_to=assigned_to_user, status=TicketStatus.NEW,
|
||||
sla_response_due=sla_response_due, sla_resolution_due=sla_resolution_due,
|
||||
created_at=datetime.utcnow(), updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(db_ticket)
|
||||
await db.commit()
|
||||
await db.refresh(db_ticket)
|
||||
|
||||
await safe_audit_log(db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
|
||||
action="ticket.create", resource_type="ticket", resource_id=db_ticket.id,
|
||||
new_values={"ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject,
|
||||
"priority": db_ticket.priority.value, "status": db_ticket.status.value})
|
||||
|
||||
return {
|
||||
"id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject,
|
||||
"title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value,
|
||||
"priority": db_ticket.priority.value, "category_id": str(db_ticket.category_id) if db_ticket.category_id else None,
|
||||
"affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None,
|
||||
"system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None,
|
||||
"contact_email": ticket.contact_email,
|
||||
"contact_phone": ticket.contact_phone,
|
||||
"created_by": str(db_ticket.created_by), "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None,
|
||||
"created_at": db_ticket.created_at, "updated_at": db_ticket.updated_at,
|
||||
"sla_response_due": db_ticket.sla_response_due,
|
||||
"sla_resolution_due": db_ticket.sla_resolution_due,
|
||||
"first_response_at": db_ticket.first_response_at,
|
||||
"resolved_at": db_ticket.resolved_at,
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid UUID format: {str(e)}")
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
last_error = e
|
||||
if "duplicate key" in str(e).lower() and "ticket_number" in str(e).lower():
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating ticket: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"No se pudo crear el ticket después de {max_retries} intentos: {str(last_error)}")
|
||||
return await ticket_service.create_ticket(ticket, current_user.tenant_id, current_user.id)
|
||||
|
||||
@router.get("/", response_model=List[TicketResponse])
|
||||
async def get_tickets(skip: int = 0, limit: int = 100, status: Optional[str] = None, priority: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Obtener tickets con filtros opcionales"""
|
||||
query = select(Ticket).where(Ticket.tenant_id == current_user.tenant_id)
|
||||
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
||||
# Solo CLIENT_USER ve únicamente sus propios tickets.
|
||||
# CLIENT_ADMIN ve todos los del tenant.
|
||||
if current_user.role == UserRole.CLIENT_USER:
|
||||
query = query.where(Ticket.created_by == current_user.id)
|
||||
|
||||
query = apply_enum_filter(query, Ticket.status, status, TicketStatus, "status")
|
||||
@@ -142,14 +71,14 @@ async def get_all_tickets_admin(skip: int = 0, limit: int = 100, status_filter:
|
||||
assigned_to_filter: Optional[str] = None, search: Optional[str] = None, date_from: Optional[str] = None,
|
||||
date_to: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Obtener todos los tickets del tenant del administrador (ADMIN/SUPPORT_MANAGER)."""
|
||||
if current_user.role not in ["ADMIN", "SUPPORT_MANAGER"]:
|
||||
if current_user.role not in (UserRole.ADMIN, UserRole.SUPPORT_MANAGER):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No tienes permisos para acceder a esta función")
|
||||
|
||||
query = select(Ticket, Tenant, User).join(Tenant, Ticket.tenant_id == Tenant.id).join(User, Ticket.created_by == User.id)
|
||||
|
||||
# SUPPORT_MANAGER solo ve su propio tenant.
|
||||
# ADMIN ve todos los tenants (es el administrador de la plataforma).
|
||||
if current_user.role == "SUPPORT_MANAGER":
|
||||
if current_user.role == UserRole.SUPPORT_MANAGER:
|
||||
query = query.where(Ticket.tenant_id == current_user.tenant_id)
|
||||
|
||||
query = apply_enum_filter(query, Ticket.status, status_filter, TicketStatus, "status")
|
||||
@@ -207,7 +136,7 @@ async def get_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), current
|
||||
"""Obtener un ticket por ID"""
|
||||
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
||||
query = select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id)
|
||||
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
||||
if current_user.role.is_client:
|
||||
query = query.where(Ticket.created_by == current_user.id)
|
||||
|
||||
query = query.options(selectinload(Ticket.category), selectinload(Ticket.affected_system), selectinload(Ticket.assigned_to_user))
|
||||
@@ -224,7 +153,7 @@ async def update_ticket(ticket_id: str, ticket: TicketUpdate, db: AsyncSession =
|
||||
"""Actualizar un ticket"""
|
||||
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
||||
query = select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id)
|
||||
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
||||
if current_user.role.is_client:
|
||||
query = query.where(Ticket.created_by == current_user.id)
|
||||
|
||||
result = await db.execute(query)
|
||||
|
||||
@@ -45,8 +45,12 @@ async def read_users(
|
||||
- role: filtrar por rol
|
||||
- is_active: filtrar por estado activo
|
||||
"""
|
||||
# ✅ CORREGIDO: Filtrar por tenant_id
|
||||
query = select(User).where(User.tenant_id == current_user.tenant_id)
|
||||
# ADMIN global ve todos los tenants; el resto solo ve su propio tenant
|
||||
from app.models.user import UserRole as _UserRole
|
||||
if current_user.role != _UserRole.ADMIN:
|
||||
query = select(User).where(User.tenant_id == current_user.tenant_id)
|
||||
else:
|
||||
query = select(User)
|
||||
|
||||
# Aplicar filtros opcionales
|
||||
if role:
|
||||
|
||||
@@ -68,11 +68,11 @@ async def generate_next_ticket_number(db: AsyncSession, tenant_id: uuid.UUID) ->
|
||||
last_ticket_number = result.scalar_one_or_none()
|
||||
|
||||
if last_ticket_number:
|
||||
last_number = int(last_ticket_number.split('-')[1])
|
||||
last_number = int(last_ticket_number.split('-')[-1])
|
||||
next_number = last_number + 1
|
||||
else:
|
||||
next_number = 1
|
||||
|
||||
|
||||
return f"TK-{next_number:06d}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user