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.pyautentica contraUsuario, verifica contraseñas con Argon2 (app/auth/security.py) y emite un JWT (create_access_token). El token se guarda en una cookiesession_tokenconhttponly,domain=.settings.domainysecuresi aplica (set_session_cookie). - Validación:
app/auth/auth_dependencies.pydefineget_token(lee header o cookie) yget_current_user. Este último decodifica el JWT, consulta Redis (redis_client) con TTL de 45 minutos y retornaUsuarioBase. Si el token no coincide conuser.tokeno el usuario está inactivo, responde 401. - Objeto de sesión:
app/models/auth/user_base.pyagrega permisos (Set[Tuple[str,str]]) y helpersaccess_control(domain, access)ydomain_any. - Inicialización: en el lifespan (
app/core/lifespan.py) se ejecutainit_dbpara crear el superusuario inicial a partir desettings.
Control de accesos¶
- Definiciones:
app/access/access_control.pyencapsula un permiso y expone.requiere, que entrega unAnnotated[bool, Depends(...)]. Así, en una ruta basta con declarar_: AccessAdmin.USUARIOS_VIEW.requierepara forzar un 403 si el usuario carece de permisos. - Enums por dominio:
app/access/access_domain.pycreaDomainAccessEnum, del que heredanAccessAdmin,AccessIngenieria,AccessConstruccion,Accesslda, etc. Cada miembro define dominio (admin,ingenieria, …), descripción y opcionalmente unNavigationMenupara construir la UI. - Menús: Los
NavigationMenuviven enapp/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 listaUsuarioBase.navigations.
Preferencias de centros de costos¶
- Cookie firmada:
app/preferences/preference_cookie.pyprovee la clase base.CentroCostosPreferenceCookie(app/preferences/centro_costos_preference.py) deriva de ella y usa la cookiecfg:centros_costos. - Dependencia:
centroCostosDepes 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.pydefinePageModel(campospage,size,search,order_by,ascending,total). - Helper para rutas:
app/utils/listing.pyexpone: auto_specs_from_modelpara generarColumnSpecautomáticamente desde modelos SQLAlchemy.dsl(search, *specs)que delega aapp/utils/search_dsl.py.paginate(session, base, page, order)que ejecutaCOUNToptimizado y retorna(total, rows).resolve_orderyorder_map_from_modelpara 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, rangosv1..v2, listasin [a,b],nin [a,b], y expresiones conOR(|) o negaciones (-). - Soporta tipos
ltreeyltree-clean, handlers personalizados (custom()) y protecciones contra queries gigantes (límite de tokens y longitud). - Extensiones:
app/utils/spec_builders.pyofrece shortcutsany_ilike_specyhas_ilike_specpara relaciones M:N y FK. - Frontend:
frontend/components/dynamic-table/search-bar.tsxguía al usuario al construir expresiones y recuerda filtros comunes (últimos 7 días, etc.).
Ejecución prolongada¶
app/core/executor.pydefinestream_executor: envuelve tareas largas en unStreamingResponse. 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_dumpfiltrados por datos (--data-onlypor defecto), los comprime (.sql.gz) y encripta (AES-GCM) produciendo.sql.gz.enccon cabeceraMAGIC. - Mantiene un buffer de backups previos (
LAST_BACKUPS=5) para “undo”. - Permite restaurar en modo
replace(trunca tablas) omerge(aplica datos encima) desactivando triggers y asegurando la extensiónltree. - Expone
restore_from_uploaded_file,undo_last_backupystream_file. app/routes/admin/database.pyintegra estas funciones y controla el cierre/limpieza del pool (engine.dispose()).
Logging y auditoría¶
app/logging/logging_setup.pyconfigura logging JSON constructlog, log rotativo diario (settings.log_file, retención configurada) y STDOUT.app/logging/access_log.pyimplementaAccessLogMiddleware:- 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(entradacentro_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.pyentrega 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.pydefine funciones de negocio (import_personal,import_procura, etc.) decoradas con@register_task. - Huey:
app/tasks/huey_consumer.pycreaRedisHuey.app/huey_task.pyimporta las tareas y valida su registro al levantar el worker. - Runner:
app/tasks/runner_task.pyexponerun_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 enjobs:last:<task_id>. - Scheduler:
app/tasks/scheduler_tick.pyse ejecuta cada 10 minutos (@huey.periodic_task(crontab(minute="*/10"))), revisa los próximos disparos (sched:indexen Redis) y encolarun_registered_task. - API de administración:
app/routes/admin/tasks.pypermite 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.pydefineBase = declarative_base(). Todos los modelos SQLAlchemy heredan de aquí (usuarios, roles, proyectos, etc.). Alembic (app/alembic/env.py) usaBase.metadatacomotarget_metadata.app/models/__init__.pyexpone el agregador de modelos para que Alembic los importe automáticamente.app/models/usuario.pydefine los modelos principales de autenticación, roles y permisos;app/models/proyectos.pycontieneCentroCostosy la tabla puenteUsuarioCentroCostos.
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. PageLayoutSesionintegra cabecera, navegación y verificación de sesión (apoya en cookies y fetch de/api/auth/me).ProjectSelectorse muestra como guard siuseProjectSessionindica 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), interfazDynamicTableRef.
Integración con API¶
- Las páginas consumen las rutas backend vía fetch declarativo. Ejemplo:
frontend/app/admin/users/page.tsxusaDynamicTablecontra/api/admin/users, complementado con modales para editar roles y centros de costos. - Muchos módulos se envuelven en
<ProjectSelector>para garantizar que la cookiecfg:centros_costosexista antes de disparar fetches. - Librerías auxiliares:
sonnerpara 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-Hosty separasub,base. resolve_tenantpermite traducir el subdominio a un tenant humano (map configurable enTENANT_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, verapp/access/. - Paginación y DSL:
PageModel,listing.py,search_dsl.py, integrados conDynamicTable. - Centros de Costos: Cookie
cfg:centros_costos, dependenciacentroCostosDep, 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:
Settingsgobierna dominios, credenciales y conexiones.
Para instrucciones operativas ve guía de usuario; para despliegues revisa despliegue.md.