import logging import os import firebase_admin from firebase_admin import credentials, storage, firestore, auth # Initialize logging logger = logging.getLogger("BIT-LOG") class FirebaseManager: """ V261.17: Manages Firebase Storage uploads for resilient audio playback. Replaces local static file serving which causes timeouts and 404s. """ _instance = None _bucket = None _db = None def __new__(cls): if cls._instance is None: cls._instance = super(FirebaseManager, cls).__new__(cls) # We delay _initialize until first actual use of db or bucket return cls._instance def _initialize(self): """Initialize Firebase Admin SDK with service account.""" if firebase_admin._apps: self._bucket = storage.bucket() self._db = firestore.client() return try: from config import FIREBASE_CREDENTIALS_PATH, STORAGE_BUCKET, IS_PRODUCTION import json logger.info("🛠️ [FIREBASE] Starting initialization...") # --- DEBUG: Print all relevant env var names --- fb_vars = [v for v in os.environ.keys() if "FIREBASE" in v or "GOOGLE" in v] logger.info(f"🔍 [FIREBASE] Detected environment variables: {fb_vars}") cred_dict = None # Try loading from environment variable first (for HF Secrets) env_creds = os.environ.get("FIREBASE_CREDENTIALS_JSON") if env_creds and len(env_creds.strip()) > 10: try: cred_dict = json.loads(env_creds) logger.info("✅ [FIREBASE] Successfully parsed credentials from FIREBASE_CREDENTIALS_JSON.") except Exception as e: logger.error(f"❌ [FIREBASE] Failed to parse FIREBASE_CREDENTIALS_JSON: {e}") # Fallback to file if environment variable not set or failed if not cred_dict: if os.path.exists(FIREBASE_CREDENTIALS_PATH): with open(FIREBASE_CREDENTIALS_PATH, "r", encoding="utf-8") as f: cred_dict = json.load(f) logger.info(f"📂 [FIREBASE] Loading credentials from file: {FIREBASE_CREDENTIALS_PATH}.") else: logger.warning(f"⚠️ [FIREBASE] No credentials found at {FIREBASE_CREDENTIALS_PATH} and env var is empty.") # We don't return here, we try to see if it was already initialized by something else if cred_dict: cred = credentials.Certificate(cred_dict) firebase_admin.initialize_app(cred, { 'storageBucket': STORAGE_BUCKET }) logger.info(f"🚀 [FIREBASE] SDK Initialized for {'PROD' if IS_PRODUCTION else 'DEV'}.") self._bucket = storage.bucket() self._db = firestore.client() logger.info("✨ [FIREBASE] Storage and Firestore clients ready.") except Exception as e: logger.error(f"🔥 [FIREBASE] Initialization failed: {e}") def get_db(self): """V2: Returns the initialized Firestore client, initializing if needed.""" if not self._db: self._initialize() return self._db def verify_token(self, id_token: str): """ V2: Verifies a Firebase Auth ID token. Returns the decoded token dictionary (including 'uid') if valid, else None. """ try: decoded_token = auth.verify_id_token(id_token) return decoded_token except Exception as e: # V285.6: Dev Fallback to prevent 401s in non-prod environments from config import IS_PRODUCTION if not IS_PRODUCTION: logger.warning(f"⚠️ [FIREBASE] Dev mode fallback: Auth check failed, using bypass user. ({e})") return {'uid': 'dev-bypass-user'} if "expired" in str(e).lower(): logger.warning("⚠️ [FIREBASE] Token expired.") else: logger.error(f"❌ [FIREBASE] Error verifying token: {e}") return None def upload_file(self, local_path: str, destination_blob_name: str) -> str: """ V273.1: Uploads local file and returns a Signed URL (CORS proof). Signed URLs bypass public access restrictions and work reliably in production. """ if not self._bucket: self._initialize() if not self._bucket: logger.error("❌ [FIREBASE] Not initialized. Cannot upload.") return None try: blob = self._bucket.blob(destination_blob_name) blob.upload_from_filename(local_path) # V273.1: Generate Signed URL instead of public_url to fix CORS import datetime signed_url = blob.generate_signed_url( version="v4", expiration=datetime.timedelta(hours=24), method="GET", ) logger.info(f"UPLOAD: [FIREBASE] Signed URL generated for {destination_blob_name}") return signed_url except Exception as e: logger.error(f"ERROR: [FIREBASE] Upload failed for {local_path}: {e}") return None # Singleton accessor firebase_manager = FirebaseManager()