""" config.py ========= Centralised application configuration. All environment variable reads happen here — once, at startup. Blueprints and helpers that need a config value use: from flask import current_app value = current_app.config['KEY'] Or for values needed at module import time (before app context): from config import Config value = Config.COMPANY_NAME """ import os from datetime import timedelta class Config: # ------------------------------------------------------------------ # # Core Flask # ------------------------------------------------------------------ # SECRET_KEY = os.environ.get('SECRET_KEY', 'change-me-in-production') SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '') SQLALCHEMY_TRACK_MODIFICATIONS = ( os.environ.get('SQLALCHEMY_TRACK_MODIFICATIONS', 'False').lower() == 'true' ) TEMPLATES_AUTO_RELOAD = ( os.environ.get('TEMPLATES_AUTO_RELOAD', 'True').lower() == 'true' ) DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true' # ------------------------------------------------------------------ # # SQLAlchemy connection pool (read from .env; safe defaults) # ------------------------------------------------------------------ # SQLALCHEMY_ENGINE_OPTIONS = { 'pool_size': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_SIZE', '10')), 'pool_timeout': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_TIMEOUT', '20')), 'pool_recycle': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_RECYCLE', '3600')), 'max_overflow': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_MAX_OVERFLOW', '20')), } # ------------------------------------------------------------------ # # Session / cookies # ------------------------------------------------------------------ # # Remember Me lifetime, and the maximum age Flask accepts for ANY session # cookie. Logins without Remember Me end after 10 hours in app.py # (adjust_session_lifetime) — do not lower this to get that, or Remember Me # cookies stop loading. PERMANENT_SESSION_LIFETIME = timedelta(days=30) SESSION_COOKIE_SECURE = ( os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true' ) SESSION_COOKIE_HTTPONLY = ( os.environ.get('SESSION_COOKIE_HTTPONLY', 'true').lower() == 'true' ) SESSION_COOKIE_SAMESITE = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax') # ------------------------------------------------------------------ # # Application identity # ------------------------------------------------------------------ # COMPANY_NAME = os.environ.get('COMPANY_NAME', 'QR Code Management System') CONTRACT_NAME = os.environ.get('CONTRACT_NAME', 'Default Contract') # ------------------------------------------------------------------ # # File uploads # ------------------------------------------------------------------ # UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/tmp') # ------------------------------------------------------------------ # # Photo verification # ------------------------------------------------------------------ # PHOTO_VERIFICATION_ENABLED = ( os.environ.get('ENABLE_PHOTO_VERIFICATION', 'true').lower() == 'true' ) DISTANCE_THRESHOLD_FOR_VERIFICATION = float( os.environ.get('PHOTO_VERIFICATION_DISTANCE_THRESHOLD', '0.3') ) VERIFICATION_PHOTO_MAX_SIZE = int( os.environ.get('VERIFICATION_PHOTO_MAX_SIZE', str(5 * 1024 * 1024)) ) # ------------------------------------------------------------------ # # Request size limits # ------------------------------------------------------------------ # # Flask 3.1 rejects any non-file form field larger than MAX_FORM_MEMORY_SIZE # (default 500 KB) with HTTP 413. The verification photo is sent as a base64 # TEXT field, so this follows the photo limit (+1 MB for the other fields). MAX_FORM_MEMORY_SIZE = VERIFICATION_PHOTO_MAX_SIZE + 1024 * 1024 # Whole-request cap (Excel imports, photos). Nginx client_max_body_size # must be at least this, or Nginx rejects larger uploads first. MAX_CONTENT_LENGTH = int(os.environ.get('MAX_UPLOAD_SIZE_MB', '50')) * 1024 * 1024 # ------------------------------------------------------------------ # # Check-in interval # ------------------------------------------------------------------ # TIME_INTERVAL = int(os.environ.get('TIME_INTERVAL', '30')) # ------------------------------------------------------------------ # # Export by Building — filtered sheet + weekly hours summary # ------------------------------------------------------------------ # # Comma-separated base employee IDs (project managers) removed from the # "Filtered Report" and "Weekly Hours by Location" sheets. BUILDING_SUMMARY_EXCLUDED_EMPLOYEE_IDS = [ e.strip() for e in os.environ.get( 'BUILDING_SUMMARY_EXCLUDED_EMPLOYEE_IDS', '4921,4944,4816,3979' ).split(',') if e.strip() ] # ------------------------------------------------------------------ # # Server # ------------------------------------------------------------------ # FLASK_HOST = os.environ.get('FLASK_HOST', '0.0.0.0') FLASK_PORT = int(os.environ.get('FLASK_PORT', '5000')) THREADED = os.environ.get('THREADED', 'True').lower() == 'true' # Number of reverse proxies in front of the app (Nginx = 1; Cloudflare in # front of Nginx = 2). ProxyFix reads the real client IP from that many # X-Forwarded-For hops. 0 = not behind a proxy (local development only). TRUSTED_PROXY_COUNT = int(os.environ.get('TRUSTED_PROXY_COUNT', '1')) # Public base URL of this deployment (e.g. https://qr.govservicesinc.com). # Used for QR images generated at startup, where there is no request. QR_BASE_URL = os.environ.get('QR_BASE_URL', '').strip() # ------------------------------------------------------------------ # # Default admin (used only on first boot) # ------------------------------------------------------------------ # DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') # ------------------------------------------------------------------ # # Remote (legacy) MySQL server — read-only source for Legacy Attendance # ------------------------------------------------------------------ # REMOTE_DB_HOST = os.environ.get('REMOTE_DB_HOST', '') REMOTE_DB_PORT = int(os.environ.get('REMOTE_DB_PORT', '3306')) REMOTE_DB_USERNAME = os.environ.get('REMOTE_DB_USERNAME', '') REMOTE_DB_PASSWORD = os.environ.get('REMOTE_DB_PASSWORD', '') REMOTE_DB_NAME = os.environ.get('REMOTE_DB_NAME', '') class DevelopmentConfig(Config): DEBUG = True SESSION_COOKIE_SECURE = False class ProductionConfig(Config): DEBUG = False TEMPLATES_AUTO_RELOAD = False # Active config selected by environment variable _config_map = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'default': Config, } def get_config(): """Return the active Config class based on FLASK_ENV.""" env = os.environ.get('FLASK_ENV', 'default').lower() return _config_map.get(env, Config)