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 STORAGE_BUCKET, IS_PRODUCTION import json logger.info("🛠️ [FIREBASE] Starting initialization...") cred_dict = None # Mission 1: Try loading from environment variables (checking multiple names for safety) creds_str = os.environ.get("FIREBASE_CREDENTIALS") or os.environ.get("FIREBASE_CREDENTIALS_JSON") if creds_str and len(creds_str.strip()) > 10: try: cred_dict = json.loads(creds_str) logger.info("✅ [FIREBASE] Successfully parsed credentials from Environment Secrets!") except Exception as e: logger.error(f"❌ [FIREBASE] Failed to parse Environment Credentials: {e}") # Fallback to local file only for local development (if no secret is set) if not cred_dict and FIREBASE_CREDENTIALS_PATH: 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] Credentials file not found at {FIREBASE_CREDENTIALS_PATH}.") if cred_dict: cred = credentials.Certificate(cred_dict) firebase_admin.initialize_app(cred, { 'storageBucket': STORAGE_BUCKET }) logger.info(f"🚀 [FIREBASE] SDK Initialized successfully for {'PROD' if IS_PRODUCTION else 'DEV'}.") else: logger.error("❌ [FIREBASE] CRITICAL ERROR: Firebase credentials not found! Firebase is OFFLINE.") 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. """ if not firebase_admin._apps: self._initialize() try: decoded_token = auth.verify_id_token(id_token) return decoded_token except Exception as e: 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()