BuddyMath / firebase_manager.py
dotandru's picture
V5.11.3 - EMERGENCY SECURITY FIX: Disable local credential files and purge secrets
5ae14ee
Raw
History Blame
5.11 kB
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()