# /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: .nexusautoparts.com or .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