70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""
|
||
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 isn’t 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)
|