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>
This commit is contained in:
2026-04-02 07:16:49 +00:00
parent bdbbc78a15
commit 6628f2deef
8 changed files with 360 additions and 20 deletions

View File

@@ -5,7 +5,7 @@ import jwt
import bcrypt
import time
from datetime import datetime, timezone, timedelta
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
from config import JWT_SECRET, JWT_ACCESS_EXPIRES, PIN_MAX_ATTEMPTS_PER_MINUTE, PIN_LOCKOUT_THRESHOLD, PIN_LOCKOUT_MINUTES
from tenant_db import get_tenant_conn, get_master_conn
@@ -48,9 +48,16 @@ def _record_attempt(device_id, success):
@auth_bp.route('/login', methods=['POST'])
def login_pin():
"""Login with tenant_id + PIN + device_id."""
"""Login with tenant_id + PIN + device_id.
tenant_id can come from:
1. Subdomain (resolved by middleware_tenant into g.tenant_id)
2. POST body tenant_id field
3. Both (subdomain takes precedence)
"""
data = request.get_json() or {}
tenant_id = data.get('tenant_id')
# Subdomain-resolved tenant takes priority over body param
tenant_id = getattr(g, 'tenant_id', None) or data.get('tenant_id')
pin = data.get('pin', '')
device_id = data.get('device_id', request.headers.get('X-Device-Id', 'unknown'))
# Optional: branch_id from the device for PIN search optimization
@@ -158,10 +165,24 @@ def login_pin():
@auth_bp.route('/employees/<int:tenant_id>', methods=['GET'])
def list_login_employees(tenant_id):
"""Public endpoint: list employees for the login screen (names + roles only, no sensitive data)."""
@auth_bp.route('/employees', methods=['GET'])
def list_login_employees(tenant_id=None):
"""Public endpoint: list employees for the login screen (names + roles only, no sensitive data).
tenant_id comes from URL path, subdomain, or ?tenant= param.
"""
# Resolve tenant_id: URL path > subdomain > query param
tid = tenant_id or getattr(g, 'tenant_id', None)
if not tid:
try:
tid = int(request.args.get('tenant', 0))
except (ValueError, TypeError):
pass
if not tid:
return jsonify({'error': 'Tenant not specified'}), 400
try:
conn = get_tenant_conn(tenant_id)
conn = get_tenant_conn(tid)
except ValueError:
return jsonify({'error': 'Tenant not found'}), 404