Jun 26 MT-1 phase

This commit is contained in:
2026-06-26 16:37:44 -04:00
parent fc39491891
commit 07cf2e5816
9 changed files with 300 additions and 55 deletions
+69
View File
@@ -0,0 +1,69 @@
"""
app/tenancy/middleware.py
-------------------------
Wires tenant resolution into the Flask request lifecycle.
`init_tenancy(app)` registers a single app-level before_request handler that:
* always clears g.tenant / g.tenant_engine (so downstream code can rely on them)
* does NOTHING further when MULTI_TENANT_ENABLED is False → today's behaviour
* bypasses static + configured exempt paths (health checks)
* otherwise resolves the Host header to a tenant and selects its engine
* returns a 404 page for an unknown / unverified / suspended host
Flask-SQLAlchemy already removes the scoped session on app-context teardown,
so each request rebinds via RoutingSession.get_bind against the fresh
g.tenant_engine — no teardown handler is needed here.
"""
from flask import g, request, current_app, Response
from app.tenancy.resolver import resolve_tenant
from app.tenancy.engine_cache import get_tenant_engine
_UNKNOWN_TENANT_PAGE = (
"<!doctype html><html lang='en'><head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
"<title>Workspace not found</title>"
"<style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;"
"background:#f6f7f9;color:#1f2937;display:flex;min-height:100vh;margin:0;"
"align-items:center;justify-content:center;text-align:center}"
".card{background:#fff;padding:2.5rem 3rem;border-radius:12px;"
"box-shadow:0 1px 4px rgba(0,0,0,.08);max-width:30rem}"
"h1{font-size:1.25rem;margin:0 0 .5rem}p{margin:.25rem 0;color:#6b7280}"
"</style></head><body><div class='card'>"
"<h1>Workspace not found</h1>"
"<p>This address isnt linked to an active JQC workspace.</p>"
"<p>Check the URL, or contact your administrator.</p>"
"</div></body></html>"
)
def _is_exempt(path):
if path.startswith('/static/'):
return True
for prefix in current_app.config.get('MULTI_TENANT_EXEMPT_PATHS', []):
if prefix and path.startswith(prefix):
return True
return False
def init_tenancy(app):
@app.before_request
def _resolve_tenant():
# Default state — referenced safely by downstream code regardless of mode.
g.tenant = None
g.tenant_engine = None
if not current_app.config.get('MULTI_TENANT_ENABLED', False):
return # inert: default database serves everything (single-tenant)
if _is_exempt(request.path):
return
host = (request.host or '').split(':')[0].strip().lower()
tenant = resolve_tenant(host)
if tenant is None:
return Response(_UNKNOWN_TENANT_PAGE, status=404, mimetype='text/html')
g.tenant = tenant
g.tenant_engine = get_tenant_engine(tenant)