05/06/2026 Initial commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
app/tenant/__init__.py — Tenant app factory (mydomain.com).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from flask import Flask
|
||||
from app.extensions import (
|
||||
db, migrate, csrf, mail, scheduler,
|
||||
tenant_login_manager, tenant_jwt, limiter,
|
||||
)
|
||||
from app.security import apply_security_headers
|
||||
from app.context import load_tenant_context, load_location_context
|
||||
from config import get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_tenant_app(config_override=None):
|
||||
flask_app = Flask(
|
||||
__name__,
|
||||
template_folder="../templates",
|
||||
static_folder="../static",
|
||||
static_url_path="/static/tenant",
|
||||
)
|
||||
|
||||
# ── Config ────────────────────────────────────────────────
|
||||
cfg = config_override or get_config()
|
||||
flask_app.config.from_object(cfg)
|
||||
|
||||
flask_app.config["SESSION_COOKIE_NAME"] = "salon_pos_tenant_session"
|
||||
flask_app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(
|
||||
seconds=int(os.environ.get("SESSION_TIMEOUT_TENANT", 1800))
|
||||
)
|
||||
flask_app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
flask_app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
|
||||
# ── Extensions ────────────────────────────────────────────
|
||||
db.init_app(flask_app)
|
||||
migrate.init_app(flask_app, db)
|
||||
csrf.init_app(flask_app)
|
||||
mail.init_app(flask_app)
|
||||
limiter.init_app(flask_app)
|
||||
|
||||
# Tenant login manager
|
||||
tenant_login_manager.login_view = "tenant_auth.login"
|
||||
tenant_login_manager.login_message_category = "warning"
|
||||
tenant_login_manager.session_protection = "strong"
|
||||
tenant_login_manager.init_app(flask_app)
|
||||
|
||||
# Tenant JWT
|
||||
tenant_jwt.init_app(flask_app)
|
||||
|
||||
# ── User loaders ──────────────────────────────────────────
|
||||
from app.models.salon import User, Staff
|
||||
|
||||
@tenant_login_manager.user_loader
|
||||
def load_tenant_user(user_id: str):
|
||||
if user_id.startswith("user:"):
|
||||
try:
|
||||
uid = int(user_id.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
return User.query.filter_by(id=uid, is_active=True)\
|
||||
.filter(User.deleted_at.is_(None)).first()
|
||||
|
||||
if user_id.startswith("staff:"):
|
||||
try:
|
||||
sid = int(user_id.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
return Staff.query.filter_by(id=sid, is_active=True)\
|
||||
.filter(Staff.deleted_at.is_(None)).first()
|
||||
|
||||
return None
|
||||
|
||||
# ── Context hooks ─────────────────────────────────────────
|
||||
flask_app.before_request(load_tenant_context)
|
||||
flask_app.before_request(load_location_context)
|
||||
|
||||
# ── Security ──────────────────────────────────────────────
|
||||
apply_security_headers(flask_app)
|
||||
|
||||
# ── Blueprints ────────────────────────────────────────────
|
||||
from app.tenant.auth.routes import tenant_auth_bp
|
||||
from app.tenant.staff_auth.routes import staff_auth_bp
|
||||
from app.tenant.checkin.routes import checkin_bp
|
||||
from app.tenant.dashboard.routes import dashboard_bp
|
||||
|
||||
flask_app.register_blueprint(tenant_auth_bp)
|
||||
flask_app.register_blueprint(staff_auth_bp)
|
||||
flask_app.register_blueprint(checkin_bp)
|
||||
flask_app.register_blueprint(dashboard_bp)
|
||||
|
||||
# Remaining blueprints registered in Phases 3–6:
|
||||
# locations, customers, appointments, services, pos, staff,
|
||||
# staff_portal, booking, waitlist, gift_cards, reviews,
|
||||
# reconciliation, inventory, marketing, reports, settings
|
||||
|
||||
# ── Import all models for Migrate ─────────────────────────
|
||||
import app.models # noqa: F401
|
||||
|
||||
logger.info("Tenant app created (env=%s)", flask_app.config.get("FLASK_ENV"))
|
||||
return flask_app
|
||||
Reference in New Issue
Block a user