Files
Autoparts-DB/pos/middleware_tenant.py
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

118 lines
3.6 KiB
Python

# /home/Autopartes/pos/middleware_tenant.py
"""Subdomain-based tenant resolver middleware for Nexus POS.
Routes like refac-lopez.nexusautoparts.com are resolved to a tenant_id
via the `tenants.subdomain` column in nexus_master.
Resolution order:
1. X-Tenant-Subdomain header (set by nginx)
2. Host header subdomain extraction (fallback for dev)
3. ?tenant=ID URL parameter (legacy / direct access)
"""
import re
from flask import request, g, redirect
from tenant_db import get_master_conn
# Domains that should NOT be treated as tenant subdomains
_RESERVED = {'www', 'api', 'admin', 'mail', 'staging', 'dev', 'nexus', 'pos', 'app', 'dashboard'}
# Cache: subdomain -> {tenant_id, name} (cleared on app restart)
_subdomain_cache = {}
def _extract_subdomain():
"""Extract tenant subdomain from request. Returns subdomain string or None."""
# 1. Nginx header (most reliable in production)
sub = request.headers.get('X-Tenant-Subdomain', '').strip().lower()
if sub and sub not in _RESERVED:
return sub
# 2. Parse from Host header (dev fallback)
host = request.host.split(':')[0].lower() # strip port
# Match: <subdomain>.nexusautoparts.com or <subdomain>.localhost
parts = host.split('.')
if len(parts) >= 3:
# e.g. refac-lopez.nexusautoparts.com -> refac-lopez
candidate = parts[0]
if candidate not in _RESERVED:
return candidate
elif len(parts) == 2 and parts[1] == 'localhost':
# e.g. refac-lopez.localhost for local dev
candidate = parts[0]
if candidate not in _RESERVED:
return candidate
return None
def _lookup_tenant_by_subdomain(subdomain):
"""Look up tenant_id and name from subdomain. Returns dict or None."""
if subdomain in _subdomain_cache:
return _subdomain_cache[subdomain]
try:
conn = get_master_conn()
cur = conn.cursor()
cur.execute(
"SELECT id, name FROM tenants WHERE LOWER(subdomain) = %s AND is_active = true",
(subdomain,)
)
row = cur.fetchone()
cur.close()
conn.close()
if row:
result = {'tenant_id': row[0], 'name': row[1]}
_subdomain_cache[subdomain] = result
return result
except Exception:
pass
return None
def clear_subdomain_cache():
"""Clear the subdomain lookup cache (call after tenant updates)."""
_subdomain_cache.clear()
def resolve_tenant():
"""Flask before_request handler: resolve tenant from subdomain or URL param.
Sets on flask.g:
- g.tenant_id (int or None)
- g.tenant_name (str or None)
- g.tenant_subdomain (str or None)
"""
g.tenant_id = None
g.tenant_name = None
g.tenant_subdomain = None
# Skip for static files, health check and Prometheus metrics scrape
if (request.path.startswith('/pos/static/')
or request.path == '/pos/health'
or request.path == '/metrics'):
return
subdomain = _extract_subdomain()
if subdomain:
tenant = _lookup_tenant_by_subdomain(subdomain)
if tenant:
g.tenant_id = tenant['tenant_id']
g.tenant_name = tenant['name']
g.tenant_subdomain = subdomain
return
else:
# Unknown subdomain: just continue without tenant (login will ask for it)
pass
# Fallback: ?tenant=ID URL parameter
tenant_param = request.args.get('tenant')
if tenant_param:
try:
g.tenant_id = int(tenant_param)
except (ValueError, TypeError):
pass