Jun 28 - Implement payment functions (Stripe)
This commit is contained in:
+61
-14
@@ -3,25 +3,37 @@ 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)
|
||||
* MT-4: checks session['impersonating_tenant_id'] and short-circuits Host
|
||||
resolution when a superadmin is impersonating a tenant
|
||||
* otherwise resolves the Host header to a tenant and selects its engine
|
||||
* returns a 404 page for an unknown / unverified / suspended host
|
||||
`init_tenancy(app)` registers two app-level before_request handlers:
|
||||
|
||||
1. `_resolve_tenant` — Host → tenant resolution (MT-1 / MT-4):
|
||||
* always clears g.tenant / g.tenant_engine
|
||||
* does NOTHING further when MULTI_TENANT_ENABLED is False → today's behaviour
|
||||
* bypasses static + configured exempt paths (health checks)
|
||||
* MT-4: checks session['impersonating_tenant_id'] and short-circuits Host
|
||||
resolution when a superadmin is impersonating a tenant
|
||||
* otherwise resolves the Host header to a tenant and selects its engine
|
||||
* returns a 404 page for an unknown / unverified / suspended host
|
||||
|
||||
2. `_billing_gate` — Stripe subscription enforcement (MT-8):
|
||||
* inert when BILLING_ENABLED=False or MULTI_TENANT_ENABLED=False
|
||||
* sets g.billing_warning for past_due tenants (banner shown in base.html)
|
||||
* redirects cancelled/suspended-by-billing tenants to /billing/suspended
|
||||
* always exempts /billing/* paths so tenants can manage their subscription
|
||||
|
||||
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, session
|
||||
import logging
|
||||
|
||||
from flask import g, request, current_app, Response, session, redirect, url_for
|
||||
|
||||
from app.tenancy.resolver import resolve_tenant
|
||||
from app.tenancy.engine_cache import get_tenant_engine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_UNKNOWN_TENANT_PAGE = (
|
||||
"<!doctype html><html lang='en'><head><meta charset='utf-8'>"
|
||||
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
||||
@@ -34,7 +46,7 @@ _UNKNOWN_TENANT_PAGE = (
|
||||
"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>This address isn't linked to an active JQC workspace.</p>"
|
||||
"<p>Check the URL, or contact your administrator.</p>"
|
||||
"</div></body></html>"
|
||||
)
|
||||
@@ -55,6 +67,7 @@ def init_tenancy(app):
|
||||
# Default state — referenced safely by downstream code regardless of mode.
|
||||
g.tenant = None
|
||||
g.tenant_engine = None
|
||||
g.billing_warning = None # MT-8: set to 'past_due' by _billing_gate when needed
|
||||
|
||||
if not current_app.config.get('MULTI_TENANT_ENABLED', False):
|
||||
return # inert: default database serves everything (single-tenant)
|
||||
@@ -91,15 +104,15 @@ def init_tenancy(app):
|
||||
allow_scheduled_reports=plan.allow_scheduled_reports if plan else True,
|
||||
allow_branding=plan.allow_branding if plan else True,
|
||||
allow_custom_domain=plan.allow_custom_domain if plan else True,
|
||||
# MT-8: billing state
|
||||
subscription_status=t.subscription_status,
|
||||
trial_ends_at=t.trial_ends_at,
|
||||
)
|
||||
g.tenant = ctx
|
||||
g.tenant_engine = get_tenant_engine(ctx)
|
||||
return # skip normal Host resolution
|
||||
except Exception:
|
||||
import logging as _logging
|
||||
_logging.getLogger(__name__).warning(
|
||||
'TENANCY | impersonation_failed | tenant_id=%s', imp_id
|
||||
)
|
||||
logger.warning('TENANCY | impersonation_failed | tenant_id=%s', imp_id)
|
||||
# Tenant not found, suspended, or engine error — clear stale session
|
||||
# keys so the next request doesn't retry a permanently failing lookup.
|
||||
session.pop('impersonating_tenant_id', None)
|
||||
@@ -113,3 +126,37 @@ def init_tenancy(app):
|
||||
|
||||
g.tenant = tenant
|
||||
g.tenant_engine = get_tenant_engine(tenant)
|
||||
|
||||
@app.before_request
|
||||
def _billing_gate():
|
||||
"""MT-8: Enforce subscription status. Inert when BILLING_ENABLED=False."""
|
||||
if not current_app.config.get('BILLING_ENABLED', False):
|
||||
return
|
||||
if not current_app.config.get('MULTI_TENANT_ENABLED', False):
|
||||
return
|
||||
|
||||
tenant = getattr(g, 'tenant', None)
|
||||
if tenant is None:
|
||||
return # resolver already handled this (404 or exempt path)
|
||||
|
||||
# Billing routes must always be reachable so tenants can manage their
|
||||
# subscription even when blocked, and the webhook can receive events.
|
||||
if (request.path.startswith('/billing/')
|
||||
or request.path.startswith('/static/')
|
||||
or _is_exempt(request.path)):
|
||||
return
|
||||
|
||||
status = tenant.subscription_status
|
||||
|
||||
if status is None or status in ('trial', 'active'):
|
||||
# Fully authorised — no action needed.
|
||||
return
|
||||
|
||||
if status == 'past_due':
|
||||
# Allow access but signal the template to show the payment warning banner.
|
||||
g.billing_warning = 'past_due'
|
||||
return
|
||||
|
||||
# status == 'cancelled' (or any unrecognised future value)
|
||||
# Block access and redirect to the subscription management page.
|
||||
return redirect(url_for('billing.suspended'))
|
||||
|
||||
Reference in New Issue
Block a user