Subir todos los cambios recientes a la rama principal

This commit is contained in:
arielit3
2026-01-16 13:39:55 -07:00
parent de5b6feef4
commit 99427cd48c
38 changed files with 15383 additions and 66 deletions

View File

@@ -55,3 +55,13 @@ async def get_current_active_superuser(
status_code=403, detail="The user doesn't have enough privileges"
)
return current_user
async def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user",
)
return current_user

View File

@@ -4,6 +4,7 @@ from sqlalchemy import select
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import uuid
import logging
from app.core.database import get_db
from app.models.category import Category
@@ -11,6 +12,8 @@ from app.api import deps
router = APIRouter()
logger = logging.getLogger("categories")
class CategoryBase(BaseModel):
name: str
description: Optional[str] = None
@@ -38,18 +41,56 @@ async def read_categories(
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
query = select(Category).offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
try:
logger.debug("Fetching categories with skip=%d, limit=%d", skip, limit)
query = select(Category).offset(skip).limit(limit)
result = await db.execute(query)
categories = result.scalars().all()
logger.debug("Fetched %d categories", len(categories))
return categories
except Exception as e:
logger.exception("Unhandled exception occurred while fetching categories.")
raise HTTPException(status_code=500, detail="Internal Server Error")
@router.post("/", response_model=CategoryResponse)
async def create_category(
category: CategoryCreate,
category: CategoryCreate,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
db_category = Category(**category.model_dump())
db.add(db_category)
new_category = Category(**category.model_dump())
db.add(new_category)
await db.commit()
await db.refresh(new_category)
return new_category
@router.put("/{category_id}", response_model=CategoryResponse)
async def update_category(
category_id: uuid.UUID,
category: CategoryUpdate,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
db_category = await db.get(Category, category_id)
if not db_category:
raise HTTPException(status_code=404, detail="Category not found")
for key, value in category.model_dump(exclude_unset=True).items():
setattr(db_category, key, value)
await db.commit()
await db.refresh(db_category)
return db_category
@router.delete("/{category_id}", status_code=204)
async def delete_category(
category_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
db_category = await db.get(Category, category_id)
if not db_category:
raise HTTPException(status_code=404, detail="Category not found")
await db.delete(db_category)
await db.commit()

View File

@@ -0,0 +1,61 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text # <--- IMPORTANTE: Necesario para consultas SQL
from app.core.database import get_db
# IMPORTANTE: Renombramos para evitar conflictos
from app.models.client import Client as ClientModel
from app.schemas.client import ClientCreate, ClientUpdate, Client as ClientSchema
router = APIRouter()
# GET: Obtener todos los clientes
@router.get("/clients", response_model=list[ClientSchema])
async def get_clients(db: AsyncSession = Depends(get_db)):
# Corrección: Usamos text() y mappings().all()
query = text("SELECT * FROM clients")
result = await db.execute(query)
return result.mappings().all()
# POST: Crear cliente
@router.post("/clients", response_model=ClientSchema)
async def create_client(client: ClientCreate, db: AsyncSession = Depends(get_db)):
# Usamos ClientModel para guardar en BD
new_client = ClientModel(**client.dict())
db.add(new_client)
await db.commit()
await db.refresh(new_client)
return new_client
# GET ONE: Obtener un cliente por ID
@router.get("/clients/{client_id}", response_model=ClientSchema)
async def read_client(client_id: int, db: AsyncSession = Depends(get_db)):
db_client = await db.get(ClientModel, client_id)
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
return db_client
# PUT: Actualizar cliente
@router.put("/clients/{client_id}", response_model=ClientSchema)
async def update_client(client_id: int, client: ClientUpdate, db: AsyncSession = Depends(get_db)):
db_client = await db.get(ClientModel, client_id)
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
for key, value in client.dict(exclude_unset=True).items():
setattr(db_client, key, value)
await db.commit()
await db.refresh(db_client)
return db_client
# DELETE: Borrar cliente
@router.delete("/clients/{client_id}")
async def delete_client(client_id: int, db: AsyncSession = Depends(get_db)):
db_client = await db.get(ClientModel, client_id)
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
await db.delete(db_client)
await db.commit()
return {"message": "Client deleted successfully"}

View File

@@ -0,0 +1,75 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
from typing import List
import uuid
from sqlalchemy.exc import NoResultFound
from app.core.database import get_db
from app.models.ticket import Ticket # Asegúrate de tener el modelo Ticket
from app.models.category import Category # Ensure Category model is imported
from app.api import deps # Para autenticación y permisos
from app.models.user import User # Asegúrate de importar el modelo User
router = APIRouter()
# Esquema para crear un ticket
class TicketCreate(BaseModel):
title: str
description: str
category_id: uuid.UUID
priority: str
# Esquema para devolver un ticket
class TicketResponse(BaseModel):
id: uuid.UUID
title: str
description: str
category_id: uuid.UUID
priority: str
created_at: str
@router.post("/", response_model=TicketResponse)
async def create_ticket(
ticket: TicketCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user) # Verifica que el usuario esté autenticado
):
# Validate if the category exists
try:
category = await db.execute(select(Category).where(Category.id == ticket.category_id))
category = category.scalar_one()
except NoResultFound:
raise HTTPException(status_code=404, detail="Category not found")
# Crear un nuevo ticket
db_ticket = Ticket(
ticket_number=str(uuid.uuid4())[:8], # Generate a unique ticket number
subject=ticket.title,
description=ticket.description,
category_id=ticket.category_id,
priority=ticket.priority,
created_by=current_user.id,
tenant_id=current_user.tenant_id
)
db.add(db_ticket)
await db.commit()
await db.refresh(db_ticket)
return db_ticket
@router.get("/", response_model=List[TicketResponse])
async def get_tickets(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user) # Verifica que el usuario esté autenticado
):
try:
result = await db.execute(select(Ticket).where(Ticket.tenant_id == current_user.tenant_id))
tickets = result.scalars().all()
return tickets
except Exception as e:
# Log the error for debugging purposes
import logging
logger = logging.getLogger("tickets")
logger.exception("Error fetching tickets for tenant_id=%s", current_user.tenant_id)
raise HTTPException(status_code=500, detail="An error occurred while fetching tickets.")

View File

@@ -5,7 +5,7 @@ Router principal para la API v1
"""
from fastapi import APIRouter
from app.api.v1.endpoints import auth, health, tenants, users, systems, categories
from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, clients, tickets
api_router = APIRouter()
@@ -45,3 +45,15 @@ api_router.include_router(
prefix="/categories",
tags=["categories"]
)
api_router.include_router(
clients.router,
prefix="/clients",
tags=["clients"]
)
api_router.include_router(
tickets.router,
prefix="/tickets",
tags=["tickets"]
)

View File

@@ -119,6 +119,18 @@ class Settings(BaseSettings):
CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
# ===================================
# CAMPOS ADICIONALES
# ===================================
POSTGRES_DB: str = Field(..., env="POSTGRES_DB")
POSTGRES_USER: str = Field(..., env="POSTGRES_USER")
POSTGRES_PASSWORD: str = Field(..., env="POSTGRES_PASSWORD")
SMTP_USERNAME: Optional[str] = Field(default=None, env="SMTP_USERNAME")
MAX_UPLOAD_SIZE: int = Field(default=10485760, env="MAX_UPLOAD_SIZE")
UPLOAD_DIR: str = Field(default="./uploads", env="UPLOAD_DIR")
RATE_LIMIT_REQUESTS: int = Field(default=100, env="RATE_LIMIT_REQUESTS")
RATE_LIMIT_WINDOW: int = Field(default=60, env="RATE_LIMIT_WINDOW")
def is_production(self) -> bool:
"""Check if environment is production."""
return self.ENVIRONMENT.lower() == "production"
@@ -139,4 +151,7 @@ def get_settings() -> Settings:
Using lru_cache to create a singleton pattern for settings.
"""
return Settings()
return Settings()
# Crear una instancia global de Settings
settings = Settings()

View File

@@ -10,6 +10,7 @@ from sqlalchemy import String, DateTime, func
from typing import AsyncGenerator
import uuid
from datetime import datetime
import logging
from app.core.config import get_settings
@@ -34,6 +35,8 @@ AsyncSessionLocal = async_sessionmaker(
autocommit=False
)
logger = logging.getLogger("database")
class Base(DeclarativeBase):
"""Base class para todos los modelos SQLAlchemy."""
@@ -57,13 +60,16 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""
async with AsyncSessionLocal() as session:
try:
logger.debug("Intentando crear una nueva sesión de base de datos.")
yield session
await session.commit()
except Exception:
except Exception as e:
logger.error("Error al crear la sesión de base de datos: %s", str(e))
await session.rollback()
raise
finally:
await session.close()
logger.debug("Sesión de base de datos cerrada.")
async def create_tables():

View File

@@ -8,10 +8,20 @@ from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
import structlog
from contextlib import asynccontextmanager
from sqlalchemy.sql import text
logger = structlog.get_logger(__name__)
# Correct the usage of async for by wrapping it in an async function
@asynccontextmanager
async def get_db_context():
from app.core.database import get_db
async for db in get_db():
yield db
class TenantMiddleware(BaseHTTPMiddleware):
"""
Middleware para extraer y validar información del tenant.
@@ -34,33 +44,56 @@ class TenantMiddleware(BaseHTTPMiddleware):
"""Process request and add tenant information."""
# Skip tenant validation for excluded paths
if request.url.path in self.EXCLUDED_PATHS or request.url.path.startswith("/docs"):
if request.url.path in self.EXCLUDED_PATHS or any(request.url.path.startswith(path) for path in self.EXCLUDED_PATHS):
return await call_next(request)
# Extract tenant from header
tenant_id = request.headers.get("X-Tenant-ID")
tenant_slug = request.headers.get("X-Tenant-Slug")
# For now, we'll be more permissive in development
# In production, tenant should be strictly required
# Revertir cambios para requerir encabezados de tenant
if not tenant_id and not tenant_slug:
logger.warning(
"Request without tenant information",
path=request.url.path,
method=request.method
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"
)
# For now, continue without tenant for development
# raise HTTPException(
# status_code=status.HTTP_400_BAD_REQUEST,
# detail="Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"
# )
# Store tenant info in request state
request.state.tenant_id = tenant_id
request.state.tenant_slug = tenant_slug
# TODO: Validate tenant exists and is active
# This would involve a database query which we'll implement later
# Validate tenant exists and is active
if tenant_id or tenant_slug:
from app.core.database import get_db
from app.models.tenant import Tenant
# Update the middleware to use the new context manager
async with get_db_context() as db:
tenant = await db.execute(
text(
"""
SELECT * FROM tenants
WHERE id = :tenant_id OR slug = :tenant_slug AND status = 'active'
"""
),
{"tenant_id": tenant_id, "tenant_slug": tenant_slug}
)
tenant = tenant.fetchone()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found or inactive"
)
# Store validated tenant info
request.state.tenant = tenant
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"
)
logger.debug(
"Tenant middleware processed",

View File

@@ -1,17 +1,17 @@
"""
Category Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean, ForeignKey
from sqlalchemy import String, Text, Boolean, ForeignKey, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
from typing import List, Optional
import uuid
from app.core.database import Base
class Category(Base):
__tablename__ = "categories"
__tablename__ = "ticket_categories"
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
@@ -20,6 +20,10 @@ class Category(Base):
# Optional: Tenant specific categories?
tenant_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True)
# Timestamp columns
created_at: Mapped[Optional[DateTime]] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[DateTime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="category")
tenant: Mapped["Tenant"] = relationship("Tenant") # Assuming Tenant model is imported

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, String
from app.core.database import Base
class Client(Base):
__tablename__ = "clients"
id = Column(Integer, primary_key=True, index=True)
clave = Column(String, nullable=False)
tipo_cliente = Column(String, nullable=False)
nombre = Column(String, nullable=False)
pais = Column(String, nullable=False)
estado = Column(String, nullable=False)
ciudad = Column(String, nullable=False)
direccion = Column(String, nullable=False)
numero_ext = Column(String, nullable=True)
cp = Column(String, nullable=True)
colonia = Column(String, nullable=True)
lada = Column(String, nullable=True)
telefono1 = Column(String, nullable=True)
telefono2 = Column(String, nullable=True)
tel_directo = Column(String, nullable=True)
ext = Column(String, nullable=True)
fax = Column(String, nullable=True)
horario = Column(String, nullable=True)
pagina = Column(String, nullable=True)
correo = Column(String, nullable=True)
medio_publicidad = Column(String, nullable=True)
nacionalidad = Column(String, nullable=True)
logo = Column(String, nullable=True)

View File

@@ -9,6 +9,8 @@ import enum
import uuid
from app.core.database import Base
from app.models.system import System
from app.models.category import Category
class TicketStatus(str, enum.Enum):
NEW = "NEW"
@@ -43,8 +45,8 @@ class Ticket(Base):
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("categories.id"), nullable=True)
affected_system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("ticket_categories.id"), nullable=True)
# Relationships
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="tickets")

View File

@@ -13,6 +13,7 @@ import uuid
from datetime import datetime
from app.core.database import Base
from app.models.ticket import Ticket
class UserRole(str, enum.Enum):

View File

@@ -0,0 +1,37 @@
from pydantic import BaseModel
class ClientBase(BaseModel):
clave: str
tipo_cliente: str
nombre: str
pais: str
estado: str
ciudad: str
direccion: str
numero_ext: str | None = None
cp: str | None = None
colonia: str | None = None
lada: str | None = None
telefono1: str | None = None
telefono2: str | None = None
tel_directo: str | None = None
ext: str | None = None
fax: str | None = None
horario: str | None = None
pagina: str | None = None
correo: str | None = None
medio_publicidad: str | None = None
nacionalidad: str | None = None
logo: str | None = None
class ClientCreate(ClientBase):
pass
class ClientUpdate(ClientBase):
pass
class Client(ClientBase):
id: int
class Config:
from_attributes = True