import logging import requests import threading import time import os from pydantic import Field, AliasChoices from pydantic_settings import BaseSettings, SettingsConfigDict # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("face-service-config") class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore" ) PROJECT_NAME: str = "FaceAttendanceAPI" MONGODB_URL: str = Field("mongodb://localhost:27017", validation_alias=AliasChoices("MONGODB_URL", "MONGODB_URI")) DB_NAME: str = "EduSync" COLLECTION_NAME: str = "students" # Model settings (Default values, will be overridden by Core Server) FACE_SIMILARITY_THRESHOLD: float = 0.48 LIVENESS_THRESHOLD: float = 0.20 BLINK_EAR_THRESHOLD: float = 0.18 # Lighting Settings LIGHTING_BRIGHTNESS_MIN: float = 60 LIGHTING_BRIGHTNESS_FAIR_LOW: float = 90 LIGHTING_BRIGHTNESS_FAIR_HIGH: float = 210 LIGHTING_BRIGHTNESS_MAX: float = 180 LIGHTING_VARIANCE_THRESHOLD: float = 5000 # Platform settings CORE_SERVER_URL: str = Field("https://edusyncserver.onrender.com", validation_alias=AliasChoices("CORE_SERVER_URL")) MODEL_PATH: str = "./models/w600k_mbf.onnx" # --- BONUS FIX #1: SECURITY INTEGRITY --- # Python API Key for server-to-server auth. # In production, must be set to a strong random string. FACE_API_KEY: str = "" def update_from_server(self): """Fetch latest config from Core Server.""" try: logger.info(f"🔄 Syncing system config from {self.CORE_SERVER_URL}") # Use API key if available headers = {"x-api-key": self.FACE_API_KEY} if self.FACE_API_KEY else {} response = requests.get(f"{self.CORE_SERVER_URL}/api/v1/config/system", headers=headers, timeout=15) if response.status_code == 200: data = response.json() if data.get("success") and "config" in data: remote_config = data["config"] mapping = { "faceSimilarityThreshold": "FACE_SIMILARITY_THRESHOLD", "livenessThreshold": "LIVENESS_THRESHOLD", "blinkEarThreshold": "BLINK_EAR_THRESHOLD", "lightingBrightnessMin": "LIGHTING_BRIGHTNESS_MIN", "lightingBrightnessFairLow": "LIGHTING_BRIGHTNESS_FAIR_LOW", "lightingBrightnessFairHigh": "LIGHTING_BRIGHTNESS_FAIR_HIGH", "lightingBrightnessMax": "LIGHTING_BRIGHTNESS_MAX", "lightingVarianceThreshold": "LIGHTING_VARIANCE_THRESHOLD" } for remote_key, local_key in mapping.items(): if remote_key in remote_config: setattr(self, local_key, remote_config[remote_key]) logger.info("✅ System config updated from server") else: logger.warning(f"âš ī¸ Failed to sync config: HTTP {response.status_code}") except Exception as e: logger.error(f"❌ Config sync failure: {e}. Using local defaults.") def start_config_sync(self): """ Starts a background thread to sync config every 10 minutes. Uses a file-based lock to ensure only ONE sync thread runs across all workers. """ lock_file = "config_sync.lock" try: # Atomic lock creation fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY) os.write(fd, str(os.getpid()).encode()) def sync_worker(): while True: time.sleep(600) # 10 mins self.update_from_server() thread = threading.Thread(target=sync_worker, daemon=True) thread.start() logger.info(f"🚀 [Worker {os.getpid()}] Config sync thread started.") except FileExistsError: # Handle stale lock try: if time.time() - os.path.getmtime(lock_file) > 900: os.remove(lock_file) return self.start_config_sync() except Exception: pass logger.info("â„šī¸ Config sync already active in another worker.") except Exception as e: logger.error(f"âš ī¸ Could not start config sync: {e}") settings = Settings()