feat(backend): Update models and endpoints configuration
- Enhanced ticket and comment models with proper relationships - Updated client_profile model for better data handling - Improved auth endpoint with better error handling - Updated main app configuration and imports - Added new dependencies to requirements.txt - Enhanced tickets endpoint with attachment support
This commit is contained in:
@@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
import structlog
|
||||
@@ -257,41 +258,49 @@ async def get_current_user(
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
# TODO: Fetch actual user from database
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload"
|
||||
)
|
||||
|
||||
# Fetch actual user from database
|
||||
query = select(User).where(User.id == user_id).options(
|
||||
selectinload(User.tenant)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User account is disabled"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": payload["sub"],
|
||||
"email": payload["email"],
|
||||
"role": payload["role"],
|
||||
"tenant_id": payload["tenant_id"]
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"role": user.role.value if hasattr(user.role, 'value') else user.role,
|
||||
"tenant_id": str(user.tenant_id),
|
||||
"tenant_name": user.tenant.name if user.tenant else None,
|
||||
"is_active": user.is_active,
|
||||
"is_two_factor_enabled": user.totp_secret is not None,
|
||||
"last_login": user.last_login.isoformat() if user.last_login else None,
|
||||
"created_at": user.created_at.isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ===================================
|
||||
# DEPENDENCIES
|
||||
# ===================================
|
||||
|
||||
async def get_current_active_user(token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
Dependency to get current active user from token.
|
||||
|
||||
Args:
|
||||
token: Access token
|
||||
|
||||
Returns:
|
||||
Current user data
|
||||
|
||||
Raises:
|
||||
HTTPException: If token is invalid or user is inactive
|
||||
"""
|
||||
payload = security.verify_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# TODO: Verify user exists and is active
|
||||
|
||||
return payload
|
||||
# Dependencies are imported from app.api.deps to avoid duplication
|
||||
# Use get_current_user and get_current_active_superuser from deps.py
|
||||
@@ -3,18 +3,24 @@ Tickets endpoints - ServiceManagerWeb
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_user
|
||||
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.category import Category # ✅ CORREGIDO: Era TicketCategory
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.category import Category
|
||||
from app.models.system import System
|
||||
from app.models.comment import TicketComment
|
||||
from app.models.attachment import TicketAttachment
|
||||
from app.api.schemas.attachment import AttachmentResponse
|
||||
from app.core.file_handler import file_handler
|
||||
import uuid
|
||||
|
||||
router = APIRouter()
|
||||
@@ -573,19 +579,60 @@ async def delete_ticket(
|
||||
return {"message": "Ticket deleted successfully"}
|
||||
|
||||
# ===================================
|
||||
# ATTACHMENT ENDPOINTS (placeholder)
|
||||
# ===================================
|
||||
# ATTACHMENT ENDPOINTS
|
||||
# ===================================
|
||||
|
||||
@router.get("/{ticket_id}/attachments")
|
||||
@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_user: User = Depends(get_current_user),
|
||||
current_tenant: Tenant = Depends(get_current_tenant)
|
||||
):
|
||||
"""
|
||||
Obtener adjuntos de un ticket
|
||||
"""
|
||||
return []
|
||||
"""Obtener adjuntos de un ticket"""
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="ID de ticket inválido")
|
||||
|
||||
# Verificar que el ticket existe y pertenece al tenant
|
||||
result = await db.execute(
|
||||
select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id)
|
||||
)
|
||||
ticket = result.scalar_one_or_none()
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(status_code=404, detail="Ticket no encontrado")
|
||||
|
||||
# Obtener attachments
|
||||
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()
|
||||
|
||||
# Construir respuesta
|
||||
response = []
|
||||
for att in attachments:
|
||||
response.append(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,
|
||||
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"
|
||||
))
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/{ticket_id}/attachments", status_code=status.HTTP_201_CREATED)
|
||||
@@ -593,12 +640,106 @@ async def upload_attachment(
|
||||
ticket_id: str,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_tenant: Tenant = Depends(get_current_tenant)
|
||||
):
|
||||
"""
|
||||
Subir un archivo adjunto a un ticket
|
||||
"""
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="File uploads not yet implemented"
|
||||
"""Subir un archivo adjunto a un ticket"""
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="ID de ticket inválido")
|
||||
|
||||
# Verificar ticket
|
||||
result = await db.execute(
|
||||
select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id)
|
||||
)
|
||||
ticket = result.scalar_one_or_none()
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(status_code=404, detail="Ticket no encontrado")
|
||||
|
||||
# Guardar archivo
|
||||
file_metadata = await file_handler.save_upload(file, current_tenant.id, ticket_uuid)
|
||||
|
||||
# Crear registro en BD
|
||||
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()
|
||||
)
|
||||
|
||||
db.add(attachment)
|
||||
await db.commit()
|
||||
await db.refresh(attachment, ["uploaded_by_user"])
|
||||
|
||||
return {
|
||||
"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_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"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""Descargar un archivo adjunto"""
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
attachment_uuid = uuid.UUID(attachment_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="ID inválido")
|
||||
|
||||
# Verificar ticket
|
||||
result = await db.execute(
|
||||
select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_tenant.id)
|
||||
)
|
||||
ticket = result.scalar_one_or_none()
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(status_code=404, detail="Ticket no encontrado")
|
||||
|
||||
# Obtener attachment
|
||||
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:
|
||||
raise HTTPException(status_code=404, detail="Adjunto no encontrado")
|
||||
|
||||
# Obtener path del archivo
|
||||
file_path = file_handler.get_file_path(attachment.file_path)
|
||||
|
||||
# Retornar archivo
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
filename=attachment.original_filename,
|
||||
media_type=attachment.mime_type
|
||||
)
|
||||
Reference in New Issue
Block a user