fix: ADMIN puede ver tickets de cualquier tenant, fixes seguridad tickets.py, logout interno

This commit is contained in:
2026-03-19 08:55:08 -06:00
parent b76fcb1390
commit 6687802f19
3 changed files with 301 additions and 212 deletions

View File

@@ -34,6 +34,7 @@ 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,
@@ -43,13 +44,16 @@ async def create_ticket(
"""Crear un nuevo ticket"""
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)):
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)
# 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)
@@ -64,22 +68,25 @@ async def get_tickets(skip: int = 0, limit: int = 100, status: Optional[str] = N
result = await db.execute(query)
tickets = result.scalars().all()
return [ticket_to_dict(t) for t in tickets]
@router.get("/admin/all", response_model=List[dict])
async def get_all_tickets_admin(skip: int = 0, limit: int = 100, status_filter: Optional[str] = None,
priority_filter: Optional[str] = None, tenant_id_filter: Optional[str] = None, category_filter: Optional[str] = None,
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)):
async def get_all_tickets_admin(
skip: int = 0, limit: int = 100,
status_filter: Optional[str] = None, priority_filter: Optional[str] = None,
tenant_id_filter: Optional[str] = None, category_filter: Optional[str] = None,
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 (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 == UserRole.SUPPORT_MANAGER:
query = query.where(Ticket.tenant_id == current_user.tenant_id)
@@ -111,7 +118,6 @@ async def get_all_tickets_admin(skip: int = 0, limit: int = 100, status_filter:
result = await db.execute(query)
rows = result.all()
# Cargar categorías en un solo query para evitar N+1
category_ids = list({ticket.category_id for ticket, _, _ in rows if ticket.category_id})
categories_map = {}
if category_ids:
@@ -127,21 +133,32 @@ async def get_all_tickets_admin(skip: int = 0, limit: int = 100, status_filter:
"category_name": categories_map.get(ticket.category_id) if ticket.category_id else None,
"created_by": str(ticket.created_by), "creator_name": f"{creator.first_name} {creator.last_name}",
"creator_email": creator.email, "assigned_to": str(ticket.assigned_to) if ticket.assigned_to 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}
"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}
for ticket, tenant, creator in rows
]
@router.get("/{ticket_id}", response_model=TicketResponse)
async def get_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def get_ticket(
ticket_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""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)
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_user.tenant_id)
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))
query = query.options(
selectinload(Ticket.category),
selectinload(Ticket.affected_system),
selectinload(Ticket.assigned_to_user)
)
result = await db.execute(query)
db_ticket = result.scalars().first()
@@ -150,11 +167,18 @@ async def get_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), current
return ticket_to_dict(db_ticket)
@router.patch("/{ticket_id}", response_model=TicketResponse)
async def update_ticket(ticket_id: str, ticket: TicketUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def update_ticket(
ticket_id: str, ticket: TicketUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""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)
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_user.tenant_id)
if current_user.role.is_client:
query = query.where(Ticket.created_by == current_user.id)
@@ -163,7 +187,11 @@ async def update_ticket(ticket_id: str, ticket: TicketUpdate, db: AsyncSession =
if not db_ticket:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found")
old_values = {"status": db_ticket.status.value, "priority": db_ticket.priority.value, "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None}
old_values = {
"status": db_ticket.status.value,
"priority": db_ticket.priority.value,
"assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None
}
update_data = ticket.dict(exclude_unset=True)
for field, value in update_data.items():
@@ -180,15 +208,26 @@ async def update_ticket(ticket_id: str, ticket: TicketUpdate, db: AsyncSession =
await db.commit()
await db.refresh(db_ticket, ["category", "affected_system", "assigned_to_user"])
new_values = {"status": db_ticket.status.value, "priority": db_ticket.priority.value, "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None}
await safe_audit_log(db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
new_values = {
"status": db_ticket.status.value,
"priority": db_ticket.priority.value,
"assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None
}
await safe_audit_log(
db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
action="ticket.update", resource_type="ticket", resource_id=db_ticket.id,
old_values=old_values, new_values=new_values)
old_values=old_values, new_values=new_values
)
return ticket_to_dict(db_ticket)
@router.patch("/{ticket_id}/close", response_model=TicketResponse)
async def close_ticket(ticket_id: str, close_request: TicketCloseRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def close_ticket(
ticket_id: str, close_request: TicketCloseRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Cerrar 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)
@@ -217,18 +256,28 @@ async def close_ticket(ticket_id: str, close_request: TicketCloseRequest, db: As
await db.commit()
await db.refresh(db_ticket, ["category", "affected_system", "assigned_to_user"])
await safe_audit_log(db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
await safe_audit_log(
db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
action="ticket.close", resource_type="ticket", resource_id=db_ticket.id,
old_values={"status": old_status}, new_values={"status": db_ticket.status.value, "resolution_notes": close_request.resolution_notes})
old_values={"status": old_status},
new_values={"status": db_ticket.status.value, "resolution_notes": close_request.resolution_notes}
)
return ticket_to_dict(db_ticket)
@router.get("/{ticket_id}/comments", response_model=List[CommentResponse])
async def get_ticket_comments(ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def get_ticket_comments(
ticket_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Obtener comentarios de 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"]:
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_user.tenant_id)
if current_user.role.is_client:
query = query.where(Ticket.created_by == current_user.id)
result = await db.execute(query)
@@ -236,23 +285,34 @@ async def get_ticket_comments(ticket_id: str, db: AsyncSession = Depends(get_db)
if not ticket_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found")
comments_query = select(TicketComment).where(TicketComment.ticket_id == ticket_uuid).options(selectinload(TicketComment.author)).order_by(TicketComment.created_at.desc())
comments_query = (
select(TicketComment)
.where(TicketComment.ticket_id == ticket_uuid)
.options(selectinload(TicketComment.author))
.order_by(TicketComment.created_at.desc())
)
result = await db.execute(comments_query)
comments = result.scalars().all()
return [
{"id": str(c.id), "ticket_id": str(c.ticket_id), "author_id": str(c.author_id),
"author_name": f"{c.author.first_name} {c.author.last_name}" if c.author else "Unknown",
"content": c.content, "is_internal": c.is_internal, "created_at": c.created_at, "updated_at": c.updated_at}
"content": c.content, "is_internal": c.is_internal,
"created_at": c.created_at, "updated_at": c.updated_at}
for c in comments
]
@router.post("/{ticket_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
async def create_comment(ticket_id: str, comment: CommentCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def create_comment(
ticket_id: str, comment: CommentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Crear un comentario en 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)
@@ -260,6 +320,13 @@ async def create_comment(ticket_id: str, comment: CommentCreate, db: AsyncSessio
if not ticket_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found")
# Fix is_internal — clientes no pueden crear comentarios internos
if comment.is_internal and current_user.role.is_client:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Clientes no pueden crear comentarios internos"
)
new_comment = TicketComment(
id=uuid.uuid4(), ticket_id=ticket_uuid, author_id=current_user.id,
content=comment.content, is_internal=comment.is_internal,
@@ -276,17 +343,26 @@ async def create_comment(ticket_id: str, comment: CommentCreate, db: AsyncSessio
await db.refresh(new_comment)
return {
"id": str(new_comment.id), "ticket_id": str(new_comment.ticket_id), "author_id": str(new_comment.author_id),
"id": str(new_comment.id), "ticket_id": str(new_comment.ticket_id),
"author_id": str(new_comment.author_id),
"author_name": f"{current_user.first_name} {current_user.last_name}",
"content": new_comment.content, "is_internal": new_comment.is_internal,
"created_at": new_comment.created_at, "updated_at": new_comment.updated_at
}
@router.delete("/{ticket_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
async def delete_ticket(
ticket_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Eliminar un ticket (solo admin/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 eliminar tickets")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No tienes permisos para eliminar tickets"
)
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
query = select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id)
result = await db.execute(query)
@@ -295,64 +371,98 @@ async def delete_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), curr
if not db_ticket:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found")
old_values = {"ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject,
"status": db_ticket.status.value, "priority": db_ticket.priority.value}
old_values = {
"ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject,
"status": db_ticket.status.value, "priority": db_ticket.priority.value
}
await db.delete(db_ticket)
await db.commit()
await safe_audit_log(db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
action="ticket.delete", resource_type="ticket", resource_id=ticket_uuid, old_values=old_values)
await safe_audit_log(
db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
action="ticket.delete", resource_type="ticket", resource_id=ticket_uuid,
old_values=old_values
)
return {"message": "Ticket deleted successfully"}
@router.get("/{ticket_id}/attachments", response_model=List[AttachmentResponse])
async def get_ticket_attachments(ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), current_tenant: Tenant = Depends(get_current_tenant)):
async def get_ticket_attachments(
ticket_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
current_tenant: Tenant = Depends(get_current_tenant)
):
"""Obtener adjuntos de un ticket"""
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
result = await db.execute(select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id))
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_tenant.id)
result = await db.execute(query)
ticket = result.scalar_one_or_none()
if not ticket:
raise HTTPException(status_code=404, detail="Ticket no encontrado")
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"] and ticket.created_by != current_user.id:
if current_user.role.is_client and ticket.created_by != current_user.id:
raise HTTPException(status_code=404, detail="Ticket no encontrado")
result = await db.execute(select(TicketAttachment).where(TicketAttachment.ticket_id == ticket_uuid).options(selectinload(TicketAttachment.uploaded_by_user)).order_by(TicketAttachment.created_at.desc()))
result = await db.execute(
select(TicketAttachment)
.where(TicketAttachment.ticket_id == ticket_uuid)
.options(selectinload(TicketAttachment.uploaded_by_user))
.order_by(TicketAttachment.created_at.desc())
)
attachments = result.scalars().all()
return [
AttachmentResponse(
id=att.id, ticket_id=att.ticket_id, comment_id=att.comment_id, uploaded_by=att.uploaded_by,
filename=att.filename, original_filename=att.original_filename, mime_type=att.mime_type,
id=att.id, ticket_id=att.ticket_id, comment_id=att.comment_id,
uploaded_by=att.uploaded_by, filename=att.filename,
original_filename=att.original_filename, mime_type=att.mime_type,
file_size=att.file_size, file_path=att.file_path,
uploaded_by_name=f"{att.uploaded_by_user.first_name} {att.uploaded_by_user.last_name}" if att.uploaded_by_user else "Unknown",
created_at=att.created_at, download_url=f"/api/v1/tickets/{ticket_id}/attachments/{att.id}/download"
created_at=att.created_at,
download_url=f"/api/v1/tickets/{ticket_id}/attachments/{att.id}/download"
) for att in attachments
]
@router.post("/{ticket_id}/attachments", status_code=status.HTTP_201_CREATED)
async def upload_attachment(ticket_id: str, file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_tenant: Tenant = Depends(get_current_tenant)):
async def upload_attachment(
ticket_id: str, file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
current_tenant: Tenant = Depends(get_current_tenant)
):
"""Subir un archivo adjunto a un ticket"""
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
result = await db.execute(select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id))
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_tenant.id)
result = await db.execute(query)
ticket = result.scalar_one_or_none()
if not ticket:
raise HTTPException(status_code=404, detail="Ticket no encontrado")
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"] and ticket.created_by != current_user.id:
if current_user.role.is_client and ticket.created_by != current_user.id:
raise HTTPException(status_code=404, detail="Ticket no encontrado")
file_metadata = await file_handler.save_upload(file, current_tenant.id, ticket_uuid)
# Para el file_handler usamos el tenant real del ticket
upload_tenant_id = ticket.tenant_id
file_metadata = await file_handler.save_upload(file, upload_tenant_id, ticket_uuid)
attachment = TicketAttachment(
id=uuid.uuid4(), ticket_id=ticket_uuid, uploaded_by=current_user.id, filename=file_metadata["filename"],
original_filename=file_metadata["original_filename"], mime_type=file_metadata["mime_type"],
file_size=file_metadata["file_size"], file_path=file_metadata["file_path"],
md5_hash=file_metadata["md5_hash"], sha256_hash=file_metadata["sha256_hash"], created_at=datetime.utcnow()
id=uuid.uuid4(), ticket_id=ticket_uuid, uploaded_by=current_user.id,
filename=file_metadata["filename"], original_filename=file_metadata["original_filename"],
mime_type=file_metadata["mime_type"], file_size=file_metadata["file_size"],
file_path=file_metadata["file_path"], md5_hash=file_metadata["md5_hash"],
sha256_hash=file_metadata["sha256_hash"], created_at=datetime.utcnow()
)
db.add(attachment)
@@ -363,58 +473,66 @@ async def upload_attachment(ticket_id: str, file: UploadFile = File(...), db: As
"success": True, "message": "Archivo subido exitosamente",
"data": AttachmentResponse(
id=attachment.id, ticket_id=attachment.ticket_id, comment_id=attachment.comment_id,
uploaded_by=attachment.uploaded_by, filename=attachment.filename, original_filename=attachment.original_filename,
mime_type=attachment.mime_type, file_size=attachment.file_size, file_path=attachment.file_path,
uploaded_by=attachment.uploaded_by, filename=attachment.filename,
original_filename=attachment.original_filename, mime_type=attachment.mime_type,
file_size=attachment.file_size, file_path=attachment.file_path,
uploaded_by_name=f"{attachment.uploaded_by_user.first_name} {attachment.uploaded_by_user.last_name}",
created_at=attachment.created_at, download_url=f"/api/v1/tickets/{ticket_id}/attachments/{attachment.id}/download"
created_at=attachment.created_at,
download_url=f"/api/v1/tickets/{ticket_id}/attachments/{attachment.id}/download"
)
}
@router.get("/{ticket_id}/attachments/{attachment_id}/download")
async def download_attachment(ticket_id: str, attachment_id: str, db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_tenant: Tenant = Depends(get_current_tenant)):
async def download_attachment(
ticket_id: str, attachment_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
current_tenant: Tenant = Depends(get_current_tenant)
):
"""Descargar un archivo adjunto"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"Download request - ticket_id: {ticket_id}, attachment_id: {attachment_id}")
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
attachment_uuid = validate_uuid_param(attachment_id, "attachment ID")
result = await db.execute(select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id))
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_tenant.id)
result = await db.execute(query)
ticket = result.scalar_one_or_none()
if not ticket:
logger.error(f"Ticket not found - ticket_id: {ticket_id}")
raise HTTPException(status_code=404, detail="Ticket no encontrado")
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"] and ticket.created_by != current_user.id:
if current_user.role.is_client and ticket.created_by != current_user.id:
raise HTTPException(status_code=404, detail="Ticket no encontrado")
result = await db.execute(select(TicketAttachment).where(TicketAttachment.id == attachment_uuid, TicketAttachment.ticket_id == ticket_uuid))
result = await db.execute(
select(TicketAttachment).where(
TicketAttachment.id == attachment_uuid,
TicketAttachment.ticket_id == ticket_uuid
)
)
attachment = result.scalar_one_or_none()
if not attachment:
logger.error(f"Attachment not found - attachment_id: {attachment_id}")
raise HTTPException(status_code=404, detail="Adjunto no encontrado")
logger.info(f"Attachment found - file_path: {attachment.file_path}, original_filename: {attachment.original_filename}")
try:
file_path = file_handler.get_file_path(attachment.file_path)
logger.info(f"Absolute file path: {file_path}")
if not file_path.exists():
logger.error(f"File does not exist at path: {file_path}")
raise HTTPException(status_code=404, detail="Archivo no encontrado en el sistema")
except Exception as e:
logger.error(f"Error getting file path: {str(e)}")
raise
logger.info(f"Returning file: {attachment.original_filename}")
return FileResponse(path=file_path, filename=attachment.original_filename, media_type=attachment.mime_type)
return FileResponse(
path=file_path,
filename=attachment.original_filename,
media_type=attachment.mime_type
)
@router.get("/{ticket_id}/issues", response_model=List[IssueResponse])
async def get_ticket_issues(
@@ -425,11 +543,9 @@ async def get_ticket_issues(
"""Obtener asuntos de un ticket."""
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
# Verificar que el ticket existe y pertenece al tenant
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
)
query = select(Ticket).where(Ticket.id == ticket_uuid)
if current_user.role != UserRole.ADMIN:
query = query.where(Ticket.tenant_id == current_user.tenant_id)
if current_user.role.is_client:
query = query.where(Ticket.created_by == current_user.id)
@@ -452,11 +568,8 @@ async def get_ticket_issues(
return [
IssueResponse(
id=issue.id,
ticket_id=issue.ticket_id,
tenant_id=issue.tenant_id,
content=issue.content,
priority=issue.priority.value,
id=issue.id, ticket_id=issue.ticket_id, tenant_id=issue.tenant_id,
content=issue.content, priority=issue.priority.value,
created_by=issue.created_by,
created_by_name=f"{issue.created_by_user.first_name} {issue.created_by_user.last_name}",
tagged_users=[
@@ -465,8 +578,7 @@ async def get_ticket_issues(
],
attachment_filename=issue.attachment_filename,
attachment_mime_type=issue.attachment_mime_type,
created_at=issue.created_at,
updated_at=issue.updated_at,
created_at=issue.created_at, updated_at=issue.updated_at,
)
for issue in issues
]
@@ -481,14 +593,9 @@ async def create_ticket_issue(
current_user: User = Depends(get_current_user),
current_tenant: Tenant = Depends(get_current_tenant),
):
"""
Crear un asunto de escalación en un ticket.
Solo puede crearlo el dueño del ticket o el CLIENT_ADMIN del tenant.
"""
"""Crear un asunto de escalación en un ticket."""
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
# 1. Verificar que el ticket existe y pertenece al tenant
result = await db.execute(
select(Ticket).where(
Ticket.id == ticket_uuid,
@@ -499,23 +606,21 @@ async def create_ticket_issue(
if not ticket_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ticket no encontrado")
# 2. Verificar permisos: solo el creador del ticket o CLIENT_ADMIN
is_ticket_owner = ticket_obj.created_by == current_user.id
is_client_admin = current_user.role == UserRole.CLIENT_ADMIN
is_staff = current_user.role.is_global
if not is_ticket_owner and not is_client_admin:
if not is_ticket_owner and not is_client_admin and not is_staff:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Solo el creador del ticket o el administrador del tenant pueden crear asuntos",
detail="Solo el creador del ticket o el administrador pueden crear asuntos",
)
# 3. Validar usuarios etiquetados — deben pertenecer al mismo tenant
tagged_users = []
if issue.tagged_user_ids:
result = await db.execute(
select(User).where(
User.id.in_(issue.tagged_user_ids),
User.tenant_id == current_user.tenant_id,
User.is_active == True,
)
)
@@ -526,10 +631,9 @@ async def create_ticket_issue(
if missing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Usuarios no encontrados en el tenant: {missing}",
detail=f"Usuarios no encontrados: {missing}",
)
# 4. Manejar adjunto opcional
attachment_path = None
attachment_filename = None
attachment_mime_type = None
@@ -539,19 +643,13 @@ async def create_ticket_issue(
attachment_filename = file_metadata["original_filename"]
attachment_mime_type = file_metadata["mime_type"]
# 5. Crear el asunto
new_issue = TicketIssue(
id=uuid.uuid4(),
ticket_id=ticket_uuid,
tenant_id=current_user.tenant_id,
created_by=current_user.id,
content=issue.content,
priority=TicketPriority[issue.priority],
attachment_path=attachment_path,
attachment_filename=attachment_filename,
id=uuid.uuid4(), ticket_id=ticket_uuid,
tenant_id=current_user.tenant_id, created_by=current_user.id,
content=issue.content, priority=TicketPriority[issue.priority],
attachment_path=attachment_path, attachment_filename=attachment_filename,
attachment_mime_type=attachment_mime_type,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
new_issue.tagged_users = tagged_users
db.add(new_issue)
@@ -559,27 +657,18 @@ async def create_ticket_issue(
await db.commit()
await db.refresh(new_issue, ["created_by_user", "tagged_users"])
# 6. Audit log
await safe_audit_log(
db=db,
tenant_id=current_user.tenant_id,
user_id=current_user.id,
action="ticket.issue.create",
resource_type="ticket_issue",
resource_id=new_issue.id,
db=db, tenant_id=current_user.tenant_id, user_id=current_user.id,
action="ticket.issue.create", resource_type="ticket_issue", resource_id=new_issue.id,
new_values={
"ticket_id": str(ticket_uuid),
"priority": issue.priority,
"ticket_id": str(ticket_uuid), "priority": issue.priority,
"tagged_users": [str(uid) for uid in issue.tagged_user_ids],
},
)
return IssueResponse(
id=new_issue.id,
ticket_id=new_issue.ticket_id,
tenant_id=new_issue.tenant_id,
content=new_issue.content,
priority=new_issue.priority.value,
id=new_issue.id, ticket_id=new_issue.ticket_id, tenant_id=new_issue.tenant_id,
content=new_issue.content, priority=new_issue.priority.value,
created_by=new_issue.created_by,
created_by_name=f"{new_issue.created_by_user.first_name} {new_issue.created_by_user.last_name}",
tagged_users=[
@@ -588,6 +677,5 @@ async def create_ticket_issue(
],
attachment_filename=new_issue.attachment_filename,
attachment_mime_type=new_issue.attachment_mime_type,
created_at=new_issue.created_at,
updated_at=new_issue.updated_at,
created_at=new_issue.created_at, updated_at=new_issue.updated_at,
)

View File

@@ -54,13 +54,13 @@ function createAuthStore() {
try {
const response = await fetch('/api/v1/auth/me', {
credentials: 'include',
headers: { 'X-App': 'client' }
headers: { 'X-App': 'internal' }
});
if (response.ok) {
const user = await response.json();
set({ user, token: null, isAuthenticated: true, isLoading: false });
}
} catch (error) {}
} catch (error) { }
}
},
login: async (credentials: LoginRequest): Promise<void> => {
@@ -94,12 +94,12 @@ function createAuthStore() {
method: 'POST',
credentials: 'include',
headers: {
'X-App': 'client',
'X-App': 'internal',
'X-Tenant-Slug': slug,
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
} catch {}
} catch { }
set(initialState);
if (typeof window !== 'undefined') {
window.location.href = '/login';

View File

@@ -1,5 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {