AgentSkillsCN

lacajita-backend

La Cajita TV FastAPI 后端开发的技能。适用于处理 API 端点、Auth0 认证、PostgreSQL 数据库、后端服务,或位于 /fastapi-playlists/ 下的 App_Mobil.py、Api.py,或 /services/ 中的文件时使用。

SKILL.md
--- frontmatter
name: lacajita-backend
description: Skill para desarrollo del backend FastAPI de La Cajita TV. Usar cuando se trabaje con endpoints API, autenticación Auth0, base de datos PostgreSQL, servicios backend, o archivos en /fastapi-playlists/ como App_Mobil.py, Api.py, o /services/.
license: MIT

La Cajita TV - Backend Skill

Guía para desarrollo del backend FastAPI + PostgreSQL + Auth0.

Stack Tecnológico

  • Python 3.12 con type hints obligatorios
  • FastAPI 0.115+ como framework web
  • PostgreSQL 15+ con psycopg2
  • Auth0 JWT RS256 para autenticación
  • Sentry para monitoreo

Arquitectura de Servicios

code
Puerto 8000: App_Mobil.py  → API Mobile (pública + autenticada)
Puerto 8001: Api.py        → API Admin Web (requiere Auth0)

Estructura del Proyecto

code
/opt/adm-caja-unified/fastapi-playlists/
├── Lacajita/
│   ├── App_Mobil/
│   │   └── App_Mobil.py     # API Mobile (~6500 líneas)
│   └── Lacajita/
│       └── Api.py           # API Admin Web
├── services/                 # Servicios compartidos
│   ├── jwplayer_service.py
│   ├── database.py
│   └── ...
├── img/                      # Archivos estáticos
│   └── livetv/              # Logos de canales
└── requirements.txt

Autenticación Auth0

Decorador de Autenticación

python
from functools import wraps
from fastapi import HTTPException, Request

def require_auth(roles: list[str] = None):
    def decorator(func):
        @wraps(func)
        async def wrapper(request: Request, *args, **kwargs):
            token = request.headers.get("Authorization", "").replace("Bearer ", "")
            if not token:
                raise HTTPException(401, "Token requerido")
            
            payload = verify_token(token)  # Valida JWT RS256
            
            if roles:
                user_roles = payload.get("https://lacajita.tv/roles", [])
                if not any(r in user_roles for r in roles):
                    raise HTTPException(403, "Permisos insuficientes")
            
            request.state.user = payload
            return await func(request, *args, **kwargs)
        return wrapper
    return decorator

Roles del Sistema

RolPermisos
adminAcceso total, gestión usuarios
producerGestión de noticias y contenido propio
agencyCrear anuncios, ver reportes
userConsumo de contenido

Patrones de Código

Endpoint con Autenticación

python
@app.get("/api/videos")
@app.get("/videos")  # Dual routing para backward compatibility
@require_auth(roles=["admin", "producer"])
async def get_videos(request: Request):
    """Obtiene lista de videos del usuario."""
    user_id = request.state.user.get("sub")
    
    async with get_db_connection() as conn:
        videos = await conn.fetch(
            "SELECT * FROM videos WHERE user_id = $1",
            user_id
        )
    
    return {"videos": [dict(v) for v in videos]}

Conexión a Base de Datos

python
import os
import psycopg2
from contextlib import contextmanager

PG_HOST = os.getenv("PG_HOST", "localhost")
PG_PORT = os.getenv("PG_PORT", "5432")
PG_DB = os.getenv("PG_DB", "lacajita_db")
PG_USER = os.getenv("PG_USER", "lacajita_app")
PG_PASSWORD = os.getenv("PG_PASSWORD")

@contextmanager
def get_db_connection():
    conn = psycopg2.connect(
        host=PG_HOST,
        port=PG_PORT,
        dbname=PG_DB,
        user=PG_USER,
        password=PG_PASSWORD
    )
    try:
        yield conn
    finally:
        conn.close()

Manejo de Errores

python
from fastapi import HTTPException
from sentry_sdk import capture_exception

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    capture_exception(exc)
    return JSONResponse(
        status_code=500,
        content={"detail": "Error interno del servidor"}
    )

Dual Routing

Para backward compatibility, todos los endpoints deben tener doble decorador:

python
@app.get("/api/playlists")   # Nueva ruta con prefijo /api
@app.get("/playlists")        # Ruta legacy sin prefijo
async def get_playlists():
    ...

Base de Datos

Tablas Principales

TablaDescripción
lacajita_playlistsPlaylists de contenido
livetv_channelsCanales de TV en vivo
news_feedNoticias
ad_requestsSolicitudes de anuncios
usersUsuarios del sistema

Migraciones

Archivos SQL en /opt/adm-caja-unified/db-migration/:

  • schema_postgres.sql - Esquema principal
  • livetv_native_system.sql - Sistema LiveTV
  • news_interactions.sql - Interacciones de noticias

Comandos

bash
# Iniciar API Admin (puerto 8001)
cd /opt/adm-caja-unified/fastapi-playlists/Lacajita
source venv/bin/activate
uvicorn Api:app --host 127.0.0.1 --port 8001 --reload

# Iniciar API Mobile (puerto 8000)
cd /opt/adm-caja-unified/fastapi-playlists/Lacajita/App_Mobil
source venv/bin/activate
uvicorn App_Mobil:app --host 127.0.0.1 --port 8000 --reload

# Health check
curl http://127.0.0.1:8001/health
curl http://127.0.0.1:8000/health

Variables de Entorno

bash
# Base de datos
PG_HOST=localhost
PG_PORT=5432
PG_DB=lacajita_db
PG_USER=lacajita_app
PG_PASSWORD=***

# Auth0
AUTH0_DOMAIN=lacajita.us.auth0.com
AUTH0_AUDIENCE=https://api.lacajita.tv

# Servicios externos
JWPLAYER_API_KEY=***
SENTRY_DSN=***

Checklist Pre-Commit

  • Type hints en todas las funciones
  • Autenticación verificada en endpoints sensibles
  • Dual routing implementado
  • Errores manejados con try-except
  • Conexiones DB cerradas correctamente