Saltar a contenido

Arquitectura y piezas clave

Visión general

 Navegador
Next.js 13 (frontend/)
    │  REST sobre HTTPS
FastAPI (app/main.py)
    ├─ Autenticación & permisos (app/auth, app/access)
    ├─ Rutas por dominio (app/routes/**)
    ├─ Utilitarios (app/utils, app/preferences)
    ├─ Tareas Huey (app/tasks)
    └─ Middleware de logging/tenants (app/logging/access_log.py)
        ├─ PostgreSQL (SQLAlchemy Base)
        ├─ Redis (sesiones, Huey, caché de usuarios)
        └─ MongoDB (ingestas específicas)

El proyecto es un monorepo con dos aplicaciones principales:

  • frontend/: Next.js con componentes reutilizables (DynamicTable, ProjectSelector) y páginas por dominio.
  • app/: FastAPI modular con dependencias explícitas para permisos, filtros y preferencias de centros de costos.

Cada servicio dockerizado se describe en despliegue.md.


Backend

Autenticación y sesión

  • Login: app/auth/auth.py autentica contra Usuario, verifica contraseñas con Argon2 (app/auth/security.py) y emite un JWT (create_access_token). El token se guarda en una cookie session_token con httponly, domain=.settings.domain y secure si aplica (set_session_cookie).
  • Validación: app/auth/auth_dependencies.py define get_token (lee header o cookie) y get_current_user. Este último decodifica el JWT, consulta Redis (redis_client) con TTL de 45 minutos y retorna UsuarioBase. Si el token no coincide con user.token o el usuario está inactivo, responde 401.
  • Objeto de sesión: app/models/auth/user_base.py agrega permisos (Set[Tuple[str,str]]) y helpers access_control(domain, access) y domain_any.
  • Inicialización: en el lifespan (app/core/lifespan.py) se ejecuta init_db para crear el superusuario inicial a partir de settings.

Control de accesos

  • Definiciones: app/access/access_control.py encapsula un permiso y expone .requiere, que entrega un Annotated[bool, Depends(...)]. Así, en una ruta basta con declarar _: AccessAdmin.USUARIOS_VIEW.requiere para forzar un 403 si el usuario carece de permisos.
  • Enums por dominio: app/access/access_domain.py crea DomainAccessEnum, del que heredan AccessAdmin, AccessIngenieria, AccessConstruccion, Accesslda, etc. Cada miembro define dominio (admin, ingenieria, …), descripción y opcionalmente un NavigationMenu para construir la UI.
  • Menús: Los NavigationMenu viven en app/models/auth/user_base.py. Durante la construcción de la sesión se vinculan a los permisos, de modo que el frontend puede renderizar la navegación en función de la lista UsuarioBase.navigations.

Preferencias de centros de costos

  • Cookie firmada: app/preferences/preference_cookie.py provee la clase base. CentroCostosPreferenceCookie (app/preferences/centro_costos_preference.py) deriva de ella y usa la cookie cfg:centros_costos.
  • Dependencia: centroCostosDep es una dependencia FastAPI (Annotated[List[str], Depends(...)]) que devuelve sólo IDs que están activos, pertenecen al usuario y siguen vigentes. Las rutas de ingeniería, construcción y LDA la usan para filtrar consultas (UnidadConstructiva, Soporte, etc.).
  • Frontend: ProjectSelector (frontend/components/project-selector.tsx) consulta /api/admin/centros_costos/me, permite seleccionar centros y persiste la elección en la cookie mediante /api/admin/centros_costos/select.

Paginación y buscadores DSL

  • Modelo de páginas: app/models/responses.py define PageModel (campos page, size, search, order_by, ascending, total).
  • Helper para rutas: app/utils/listing.py expone:
  • auto_specs_from_model para generar ColumnSpec automáticamente desde modelos SQLAlchemy.
  • dsl(search, *specs) que delega a app/utils/search_dsl.py.
  • paginate(session, base, page, order) que ejecuta COUNT optimizado y retorna (total, rows).
  • resolve_order y order_map_from_model para mapear llaves front-end a columnas ordenables.
  • DSL (app/utils/search_dsl.py):
  • Opera sobre ColumnSpec (texto, número, fechas, booleanos, ltree, etc.).
  • Admite operadores ~=, ^=, $=, =, !=, comparadores, rangos v1..v2, listas in [a,b], nin [a,b], y expresiones con OR (|) o negaciones (-).
  • Soporta tipos ltree y ltree-clean, handlers personalizados (custom()) y protecciones contra queries gigantes (límite de tokens y longitud).
  • Extensiones: app/utils/spec_builders.py ofrece shortcuts any_ilike_spec y has_ilike_spec para relaciones M:N y FK.
  • Frontend: frontend/components/dynamic-table/search-bar.tsx guía al usuario al construir expresiones y recuerda filtros comunes (últimos 7 días, etc.).

Ejecución prolongada

  • app/core/executor.py define stream_executor: envuelve tareas largas en un StreamingResponse. Emite padding inicial, mantiene pings para evitar timeouts y serializa el resultado (o error) como JSON. Lo usa, por ejemplo, la restauración de bases (app/routes/admin/database.py).

Respaldo y restauración de base de datos

  • app/utils/database_dump.py:
  • Genera claves AES-256 (generate_key).
  • Crea dumps pg_dump filtrados por datos (--data-only por defecto), los comprime (.sql.gz) y encripta (AES-GCM) produciendo .sql.gz.enc con cabecera MAGIC.
  • Mantiene un buffer de backups previos (LAST_BACKUPS=5) para “undo”.
  • Permite restaurar en modo replace (trunca tablas) o merge (aplica datos encima) desactivando triggers y asegurando la extensión ltree.
  • Expone restore_from_uploaded_file, undo_last_backup y stream_file.
  • app/routes/admin/database.py integra estas funciones y controla el cierre/limpieza del pool (engine.dispose()).

Logging y auditoría

  • app/logging/logging_setup.py configura logging JSON con structlog, log rotativo diario (settings.log_file, retención configurada) y STDOUT.
  • app/logging/access_log.py implementa AccessLogMiddleware:
  • Genera request_id.
  • Identifica host, subdominio y mapea a un tenant (TENANT_WHITELIST).
  • Extrae usuario y token desde la cookie JWT.
  • Intenta leer la cookie cfg:centros_costos (entrada centro_costos_preference).
  • Captura bodies/respuestas pequeñas y los añade al log.
  • El log final es un JSON con status, dur_ms, client_ip, query, body, resp, etc.
  • Lectura de logs: app/routes/admin/logs.py entrega un panel completo para:
  • Paginar JSON logs (con filtros por fecha, status, subdominio, usuario, centros de costos, etc.).
  • Analizar métricas (duración promedio, breakdown por status/método).
  • Resolver información de IPs (ip_address), descargar traces y enriquecer con datos de usuario.

Tareas y programador Huey

  • Registro: app/tasks/tasks.py define funciones de negocio (import_personal, import_procura, etc.) decoradas con @register_task.
  • Huey: app/tasks/huey_consumer.py crea RedisHuey. app/huey_task.py importa las tareas y valida su registro al levantar el worker.
  • Runner: app/tasks/runner_task.py expone run_registered_task (tarea Huey). Usa locks en Redis (jobs:lock:<task_id>) para evitar ejecuciones simultáneas, captura stdout/stderr en memoria y guarda el resultado en jobs:last:<task_id>.
  • Scheduler: app/tasks/scheduler_tick.py se ejecuta cada 10 minutos (@huey.periodic_task(crontab(minute="*/10"))), revisa los próximos disparos (sched:index en Redis) y encola run_registered_task.
  • API de administración: app/routes/admin/tasks.py permite listar tareas registradas, programar por fecha o intervalo, cancelar, lanzar manualmente y leer el último log o el estado del lock.

Modelos y migraciones

  • app/models/base.py define Base = declarative_base(). Todos los modelos SQLAlchemy heredan de aquí (usuarios, roles, proyectos, etc.). Alembic (app/alembic/env.py) usa Base.metadata como target_metadata.
  • app/models/__init__.py expone el agregador de modelos para que Alembic los importe automáticamente.
  • app/models/usuario.py define los modelos principales de autenticación, roles y permisos; app/models/proyectos.py contiene CentroCostos y la tabla puente UsuarioCentroCostos.

Configuración (Settings)

app/core/settings.py define una clase Pydantic con computed_field para valores derivados:

  • Información general (project_name, stack_name, domain, production, utc_offset).
  • Logging (log_level, log_retention_days, log_file).
  • Seguridad (algorithm, secret_key, expiración de tokens, dominios de email).
  • Credenciales iniciales del superusuario.
  • Conexiones: Redis, PostgreSQL (database_uri), MongoDB (mongodb_uri), OpenAI.
  • Correo SMTP (mail_config), uploads (uploads_dir).

Las variables se cargan desde .env. get_settings() usa lru_cache() para reutilizar la instancia.

Otros utilitarios relevantes

  • app/utils/email.py: wrappers para FastAPI-Mail.
  • app/utils/material_search.py: filtros específicos para catálogos de materiales.
  • app/utils/process_img.py: normalización de imágenes subidas.
  • app/utils/utc_time.py: helper para convertir datetimes a UTC consistente.
  • app/core/mongo.py: gestiona cliente global de MongoDB y dependencias FastAPI.

Frontend

Infraestructura Next.js

  • Usa la carpeta frontend/app/ (app router). Secciones notables: admin, ingenieria, construccion, lda.
  • PageLayoutSesion integra cabecera, navegación y verificación de sesión (apoya en cookies y fetch de /api/auth/me).
  • ProjectSelector se muestra como guard si useProjectSession indica que no hay selección vigente.

DynamicTable y DSL

  • Archivo principal: frontend/components/dynamic-table/dynamic-table.tsx (≈900 líneas) con:
  • Estado de paginación, orden y búsqueda (initialPage, initialOrderBy, etc.).
  • Debounce de búsqueda (useDebounce), manejo de loading, errores y totales.
  • Columnas dinámicas con renderizadores (CellRenderer), exportaciones a Excel/PDF, vista Gantt opcional.
  • Context menu configurable (MenuItems, RowActionsMenuButton).
  • frontend/components/dynamic-table/search-bar.tsx: UI para construir expresiones DSL, atajos de rango de fechas, badges con filtros activos y sugerencias por tipo de columna.
  • frontend/components/dynamic-table/types.ts: tipado de columnas (text, number, ltree, etc.), respuestas API (ApiResponse), interfaz DynamicTableRef.

Integración con API

  • Las páginas consumen las rutas backend vía fetch declarativo. Ejemplo: frontend/app/admin/users/page.tsx usa DynamicTable contra /api/admin/users, complementado con modales para editar roles y centros de costos.
  • Muchos módulos se envuelven en <ProjectSelector> para garantizar que la cookie cfg:centros_costos exista antes de disparar fetches.
  • Librerías auxiliares:
  • sonner para toasts.
  • handleErrorResponse (frontend/lib/handleError.ts) para normalizar errores HTTP.
  • useProjectSession (frontend/lib/project-client.ts) usa Zustand para cachear la selección de centros.

Middleware y subdominios

AccessLogMiddleware es también responsable de identificar subdominios:

  • Inspecciona Host/X-Forwarded-Host y separa sub, base.
  • resolve_tenant permite traducir el subdominio a un tenant humano (map configurable en TENANT_WHITELIST).
  • Inserta estos datos en el contexto de structlog, de manera que se reflejan en los logs consumidos por /api/admin/logs.

La cookie session_token se asigna con domain=.settings.domain, habilitando el inicio de sesión único entre auth.<dominio>, app.<dominio>, etc.


Resumen

  • Sesión: JWT + cookie httponly, cacheado en Redis (get_current_user).
  • Permisos: Enums de acceso con .requiere, ver app/access/.
  • Paginación y DSL: PageModel, listing.py, search_dsl.py, integrados con DynamicTable.
  • Centros de Costos: Cookie cfg:centros_costos, dependencia centroCostosDep, selector visual.
  • Tareas: Huey + Redis, programadas vía /api/admin/tasks.
  • Backups: Dumps cifrados AES-GCM, restauración con streams.
  • Logs: Middleware con enriquecimiento de contexto, panel de análisis.
  • Configuración: Settings gobierna dominios, credenciales y conexiones.

Para instrucciones operativas ve guía de usuario; para despliegues revisa despliegue.md.