Files
Autoparts-DB/pos/middleware_tenant.py
consultoria-as a236187f3a feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search
- New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py
- New marketplace_external.html/js with ML management UI
- Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors
- Inventory: new .btn--meli styles, select/label CSS fixes
- WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog
- DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue
- Add Celery tasks for ML sync and webhook processing
- Sidebar: MercadoLibre navigation link
2026-05-26 04:24:07 +00:00

116 lines
3.5 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 and health check
if request.path.startswith('/pos/static/') or request.path == '/pos/health':
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