Files
Autoparts-DB/pos/middleware_tenant.py
consultoria-as 6628f2deef feat: subdomain routing por tenant — refac-xxx.nexusautoparts.com
- Nginx wildcard config: *.nexusautoparts.com routes to POS app with X-Tenant-Subdomain header
- middleware_tenant.py: resolves subdomain -> tenant_id via nexus_master.tenants.subdomain
- auth_bp: login and employee list endpoints accept tenant from subdomain, URL param, or body
- login.html: auto-detects tenant from subdomain, shows business name, falls back to ?tenant=ID
- tenant_manager: generates subdomain slug from business name on provision_tenant()
- Migration v1.2: adds subdomain column + unique index to tenants table
- setup-nginx.sh: one-command install script for the nginx config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 07:16:49 +00:00

117 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'}
# 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 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: redirect to main site (only for page loads, not API)
if not request.path.startswith('/pos/api/'):
return redirect('https://nexusautoparts.com', code=302)
# 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