- 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
116 lines
3.5 KiB
Python
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
|