diff --git a/backend/api/server_fastapi_router.py b/backend/api/server_fastapi_router.py index 256c1eb..fbaf612 100644 --- a/backend/api/server_fastapi_router.py +++ b/backend/api/server_fastapi_router.py @@ -391,11 +391,12 @@ async def authorize_device( ) # Verify Firebase token - user_info = self.server_instance.auth_connector.verify_firebase_token(firebase_id_token) - if not user_info: + try: + user_info = self.server_instance.auth_connector.verify_firebase_token(firebase_id_token) + except ValueError as e: raise HTTPException( status_code=401, - detail="Invalid Firebase token" + detail=str(e) ) user_id = user_info["user_id"] diff --git a/backend/auth/auth_connector.py b/backend/auth/auth_connector.py index 6fe9886..9870126 100644 --- a/backend/auth/auth_connector.py +++ b/backend/auth/auth_connector.py @@ -249,7 +249,9 @@ def delete_device_code(self, device_code: str) -> bool: def verify_firebase_token(self, id_token: str) -> Optional[Dict[str, Any]]: """Verify Firebase ID token from website/plugin.""" try: + logger.info(f"Verifying Firebase token (length={len(id_token)}, prefix={id_token[:20]}...)") decoded_token = auth.verify_id_token(id_token) + logger.info(f"Firebase token verified for uid: {decoded_token['uid']}") return { "user_id": decoded_token['uid'], "email": decoded_token.get('email'), @@ -257,13 +259,16 @@ def verify_firebase_token(self, id_token: str) -> Optional[Dict[str, Any]]: } except auth.InvalidIdTokenError as e: logger.error(f"Invalid Firebase token: {e}") - return None + raise ValueError(f"Invalid token: {e}") except auth.ExpiredIdTokenError as e: logger.error(f"Expired Firebase token: {e}") - return None + raise ValueError(f"Expired token: {e}") except auth.RevokedIdTokenError as e: logger.error(f"Revoked Firebase token: {e}") - return None + raise ValueError(f"Revoked token: {e}") except auth.CertificateFetchError as e: logger.error(f"Firebase certificate fetch error: {e}") - return None + raise ValueError(f"Certificate fetch error: {e}") + except Exception as e: + logger.error(f"Unexpected Firebase token error: {e}") + raise ValueError(f"Token verification failed: {e}") diff --git a/backend/cli.py b/backend/cli.py index 7227512..8f215b7 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -1,17 +1,20 @@ """CLI for serving Modal apps locally.""" +import os import signal import subprocess import sys +import threading +from pathlib import Path # Dev combined app - all services in one for local iteration DEV_COMBINED_APP = "apps/dev_combined.py" # Individual apps for staging/prod deployment APPS = { - "server": ("services/http_server.py", "\033[36m"), # Cyan - "search": ("services/search_service.py", "\033[33m"), # Yellow - "processing": ("services/processing_service.py", "\033[35m"), # Magenta + "server": ("apps/server.py", "\033[36m"), # Cyan + "search": ("apps/search_app.py", "\033[33m"), # Yellow + "processing": ("apps/processing_app.py", "\033[35m"), # Magenta } RESET = "\033[0m" @@ -41,14 +44,22 @@ def serve_all(): print("Note: For staging/prod, deploy individual apps separately.\n") print("-" * 60 + "\n") + # Ensure venv bin is in PATH for Modal subprocess calls + venv_bin = Path(__file__).parent / ".venv" / "bin" + env = os.environ.copy() + if venv_bin.exists(): + current_path = env.get("PATH", "") + env["PATH"] = f"{venv_bin}:{current_path}" if current_path else str(venv_bin) + # Run with color-coded output prefixing color = "\033[32m" # Green for combined dev app process = subprocess.Popen( - ["modal", "serve", DEV_COMBINED_APP], + ["uv", "run", "modal", "serve", DEV_COMBINED_APP], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, + env=env, ) # Handle graceful shutdown @@ -68,12 +79,20 @@ def _serve_single_app(name: str): """Serve a single app with color-coded output.""" path, color = APPS[name] + # Ensure venv bin is in PATH for Modal subprocess calls + venv_bin = Path(__file__).parent / ".venv" / "bin" + env = os.environ.copy() + if venv_bin.exists(): + current_path = env.get("PATH", "") + env["PATH"] = f"{venv_bin}:{current_path}" if current_path else str(venv_bin) + process = subprocess.Popen( - ["modal", "serve", path], + ["uv", "run", "modal", "serve", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, + env=env, ) # Handle graceful shutdown @@ -102,3 +121,61 @@ def serve_search(): def serve_processing(): """Serve the processing app.""" _serve_single_app("processing") + + +def serve_staging(): + """ + Serve all staging apps concurrently (server + search + processing). + + Runs all three apps in separate processes with ENVIRONMENT=staging. + This matches the production architecture but runs locally. + """ + print("Starting staging apps (all services separately)...\n") + print(f" \033[36m●{RESET} server\n") + print(f" \033[33m●{RESET} search\n") + print(f" \033[35m●{RESET} processing\n") + print("Note: Cross-app communication works between these deployed apps.\n") + print("-" * 60 + "\n") + + # Ensure venv bin is in PATH for Modal subprocess calls + venv_bin = Path(__file__).parent / ".venv" / "bin" + env = os.environ.copy() + env["ENVIRONMENT"] = "staging" + if venv_bin.exists(): + current_path = env.get("PATH", "") + env["PATH"] = f"{venv_bin}:{current_path}" if current_path else str(venv_bin) + + # Start all three processes + processes = [] + for name in ["server", "search", "processing"]: + path, color = APPS[name] + process = subprocess.Popen( + ["uv", "run", "modal", "serve", path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + env=env, + ) + processes.append((process, name, color)) + + # Handle graceful shutdown + def signal_handler(sig, frame): + for process, _, _ in processes: + process.terminate() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Stream output from all processes with color prefixing + threads = [] + for process, name, color in processes: + thread = threading.Thread(target=_prefix_output, args=(process, name, color)) + thread.daemon = True + thread.start() + threads.append(thread) + + # Wait for all processes + for process, _, _ in processes: + process.wait() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e02da15..8f2a1f4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ [project.scripts] dev = "cli:serve_all" +staging = "cli:serve_staging" server = "cli:serve_server" search = "cli:serve_search" processing = "cli:serve_processing" diff --git a/backend/services/http_server.py b/backend/services/http_server.py index 7c9d72a..f9e3f91 100644 --- a/backend/services/http_server.py +++ b/backend/services/http_server.py @@ -30,17 +30,18 @@ def _initialize_connectors(self): # Initialize Firebase Admin SDK (required for token verification) try: import firebase_admin - import json - firebase_credentials = json.loads(get_env_var("FIREBASE_ADMIN_KEY")) - from firebase_admin import credentials - cred = credentials.Certificate(firebase_credentials) - firebase_admin.initialize_app(cred) - logger.info(f"[{self.__class__.__name__}] Firebase Admin SDK initialized") - except ValueError: - # Already initialized, which is fine - pass + if not firebase_admin._apps: + import json + firebase_credentials = json.loads(get_env_var("FIREBASE_SERVICE_ACCOUNT_JSON")) + from firebase_admin import credentials + cred = credentials.Certificate(firebase_credentials) + firebase_admin.initialize_app(cred) + logger.info(f"[{self.__class__.__name__}] Firebase Admin SDK initialized") + else: + logger.info(f"[{self.__class__.__name__}] Firebase Admin SDK already initialized") except Exception as e: - logger.warning(f"[{self.__class__.__name__}] Firebase initialization failed: {e}") + logger.error(f"[{self.__class__.__name__}] Firebase initialization failed: {e}") + raise # Get environment variables PINECONE_API_KEY = get_env_var("PINECONE_API_KEY")