Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/api/server_fastapi_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
13 changes: 9 additions & 4 deletions backend/auth/auth_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,21 +249,26 @@ 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'),
"email_verified": decoded_token.get('email_verified', False)
}
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}")
87 changes: 82 additions & 5 deletions backend/cli.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 11 additions & 10 deletions backend/services/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down