""" 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): import os as _os # Resolve paths relative to this file to avoid CWD-dependent behaviour. # app/tenant/__init__.py → ../../templates = /templates _here = _os.path.dirname(_os.path.abspath(__file__)) _root = _os.path.normpath(_os.path.join(_here, "..", "..")) flask_app = Flask( __name__, template_folder=_os.path.join(_root, "templates"), static_folder=_os.path.join(_root, "static", "tenant"), static_url_path="/static", ) # ── 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 from app.tenant.locations.routes import locations_bp from app.tenant.customers.routes import customers_bp from app.tenant.services.routes import services_bp from app.tenant.appointments.routes import appointments_bp from app.tenant.pos.routes import pos_bp from app.tenant.gift_cards.routes import gift_cards_bp from app.tenant.staff.routes import staff_bp from app.tenant.staff_portal.routes import staff_portal_bp from app.tenant.booking.routes import booking_bp from app.tenant.waitlist.routes import waitlist_bp from app.tenant.reviews.routes import reviews_bp from app.tenant.reconciliation.routes import reconciliation_bp from app.tenant.settings.routes import settings_bp for bp in [ tenant_auth_bp, staff_auth_bp, checkin_bp, dashboard_bp, locations_bp, customers_bp, services_bp, appointments_bp, pos_bp, gift_cards_bp, staff_bp, staff_portal_bp, booking_bp, waitlist_bp, reviews_bp, reconciliation_bp, settings_bp, ]: flask_app.register_blueprint(bp) # Phase 4+ stubs (inventory, marketing, reports) from app.tenant.inventory.routes import inventory_bp from app.tenant.marketing.routes import marketing_bp from app.tenant.reports.routes import reports_bp from app.tenant.pay_periods.routes import pay_periods_bp flask_app.register_blueprint(inventory_bp) flask_app.register_blueprint(marketing_bp) flask_app.register_blueprint(reports_bp) flask_app.register_blueprint(pay_periods_bp) # ── APScheduler ─────────────────────────────────────────── flask_app.config.setdefault("SCHEDULER_API_ENABLED", False) flask_app.config.setdefault("SCHEDULER_TIMEZONE", "UTC") from app.scheduler_jobs import send_appointment_reminders, send_review_requests if not scheduler.running: scheduler.init_app(flask_app) scheduler.add_job( id="appointment_reminders", func=send_appointment_reminders, args=[flask_app], trigger="interval", minutes=5, replace_existing=True, ) scheduler.add_job( id="review_requests", func=send_review_requests, args=[flask_app], trigger="interval", minutes=10, replace_existing=True, ) scheduler.start() # ── 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