import os class Config: # Build the SQLAlchemy URI from discrete env vars, or accept a full DATABASE_URL. DB_USER = os.environ.get("DB_USER", "jqc_features") DB_PASSWORD = os.environ.get("DB_PASSWORD", "") DB_HOST = os.environ.get("DB_HOST", "127.0.0.1") DB_PORT = os.environ.get("DB_PORT", "3306") DB_NAME = os.environ.get("DB_NAME", "jqc_features") SQLALCHEMY_DATABASE_URI = os.environ.get( "DATABASE_URL", f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4", ) SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True, "pool_recycle": 280} # Public contact button target shown in the closing CTA. Still used as the # "or just email us" fallback link under the booking form. DEMO_CONTACT_URL = os.environ.get("DEMO_CONTACT_URL", "mailto:info@ltservicesinc.com") # --- Demo appointment requests --- # Where the owner notification is sent. Blank -> no mail is sent, the # request is still stored and visible in /admin/demos. DEMO_NOTIFY_EMAIL = os.environ.get("DEMO_NOTIFY_EMAIL", "") # Send the customer a "we got it" copy as well. DEMO_CONFIRM_CUSTOMER = os.environ.get("DEMO_CONFIRM_CUSTOMER", "1") == "1" # Bookable window, local business hours. Slots are generated every # DEMO_SLOT_MINUTES from DEMO_HOUR_START up to (not including) DEMO_HOUR_END. DEMO_HOUR_START = int(os.environ.get("DEMO_HOUR_START", "8")) DEMO_HOUR_END = int(os.environ.get("DEMO_HOUR_END", "17")) DEMO_SLOT_MINUTES = int(os.environ.get("DEMO_SLOT_MINUTES", "30")) # How far ahead a customer may book, in days. DEMO_MAX_DAYS_AHEAD = int(os.environ.get("DEMO_MAX_DAYS_AHEAD", "90")) # Label only — dates/times are stored exactly as the customer picked them. DEMO_TIMEZONE_LABEL = os.environ.get("DEMO_TIMEZONE_LABEL", "Local time") # Max requests accepted from one IP per hour (per gunicorn worker). DEMO_RATE_LIMIT = int(os.environ.get("DEMO_RATE_LIMIT", "5")) # --- Outgoing mail (SMTP) --- SMTP_HOST = os.environ.get("SMTP_HOST", "") SMTP_PORT = int(os.environ.get("SMTP_PORT", "587")) SMTP_USER = os.environ.get("SMTP_USER", "") SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "1") == "1" # STARTTLS on 587 SMTP_USE_SSL = os.environ.get("SMTP_USE_SSL", "0") == "1" # implicit TLS on 465 SMTP_TIMEOUT = int(os.environ.get("SMTP_TIMEOUT", "20")) MAIL_FROM = os.environ.get("MAIL_FROM", "") or SMTP_USER MAIL_SUBJECT_PREFIX = os.environ.get("MAIL_SUBJECT_PREFIX", "[JQC]") # --- Admin / session --- # SECRET_KEY signs session cookies and CSRF tokens. MUST be set in production. SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-insecure-change-me") # Single admin account. Password is stored ONLY as a Werkzeug hash — never plaintext. # Generate a hash: python3 -c "from werkzeug.security import generate_password_hash as g; print(g('yourpassword'))" ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "admin") ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "") # Cookie hardening. SESSION_COOKIE_SECURE must be true once served over HTTPS. SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" SESSION_COOKIE_SECURE = os.environ.get("SESSION_COOKIE_SECURE", "1") == "1" # Path to the auth log fail2ban watches. Empty -> /logs/auth.log AUTH_LOG_PATH = os.environ.get("AUTH_LOG_PATH", "") # CSRF token lifetime in seconds. Flask-WTF defaults to 3600 (1h), which can # 400 a long edit on save. Blank/None -> token is valid for the whole session # (still single-use-per-session and cookie-bound). Set an integer to override. _csrf_ttl = os.environ.get("WTF_CSRF_TIME_LIMIT", "").strip() WTF_CSRF_TIME_LIMIT = int(_csrf_ttl) if _csrf_ttl.isdigit() else None