Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
90 changes: 83 additions & 7 deletions backend/api/server_fastapi_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import uuid

import modal
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi import APIRouter, Body, File, Form, HTTPException, UploadFile

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -93,6 +93,7 @@ def _register_routes(self):
self.router.add_api_route("/cache/clear", self.clear_cache, methods=["POST"])
self.router.add_api_route("/auth/device/code", self.request_device_code, methods=["POST"])
self.router.add_api_route("/auth/device/poll", self.poll_device_code, methods=["POST"])
self.router.add_api_route("/auth/device/authorize", self.authorize_device, methods=["POST"])

async def health(self):
"""
Expand Down Expand Up @@ -310,28 +311,28 @@ async def request_device_code(self):
logger.error(f"[Device Code] Error generating device code: {e}")
raise HTTPException(status_code=500, detail=str(e))

async def poll_device_code(self, device_code: str):
async def poll_device_code(self, device_code: str = Body(..., embed=True)):
"""
Poll for device code authorization status.

Request body:
{
"device_code": "a8f3j2k1..."
}

Responses:
- Still waiting: {"status": "pending"}
- User authorized: {"status": "authorized", "user_id": "...", "id_token": "...", "refresh_token": "..."}
- Timed out: {"status": "expired", "error": "device_code_expired"}
- User denied: {"status": "denied", "error": "user_denied_authorization"}

Polling behavior:
- Client should poll every 3 seconds (interval from device/code response)
- Max 200 attempts (10 minutes total)
- Stop immediately if user closes dialog
"""
try:

if not device_code:
raise HTTPException(
status_code=400,
Expand All @@ -349,9 +350,84 @@ async def poll_device_code(self, device_code: str):

logger.info(f"[Device Poll] Device code {device_code} | status: {status.get('status')}")
return status

except HTTPException:
raise
except Exception as e:
logger.error(f"[Device Poll] Error polling device code: {e}")
raise HTTPException(status_code=500, detail=str(e))

async def authorize_device(
self,
user_code: str = Body(...),
firebase_id_token: str = Body(...),
firebase_refresh_token: str = Body("")
):
"""
Authorize a device after user logs in on website.

Request body:
{
"user_code": "ABC-420",
"firebase_id_token": "eyJhbGc...",
"firebase_refresh_token": "AOEOulbB..." (optional for now)
}

Response:
- Success: {"success": true}
- Errors: 400 (missing fields), 401 (invalid token), 404 (code not found), 500 (server error)
"""
try:
# Validate required fields
if not user_code:
raise HTTPException(
status_code=400,
detail="Missing required field: 'user_code'"
)
if not firebase_id_token:
raise HTTPException(
status_code=400,
detail="Missing required field: 'firebase_id_token'"
)

# Verify Firebase token
user_info = self.server_instance.auth_connector.verify_firebase_token(firebase_id_token)
if not user_info:
raise HTTPException(
status_code=401,
detail="Invalid Firebase token"
)

user_id = user_info["user_id"]
logger.info(f"[Device Authorize] Verified token for user: {user_id}")

# Lookup device_code from user_code
device_code = self.server_instance.auth_connector.get_device_code_by_user_code(user_code)
if not device_code:
raise HTTPException(
status_code=404,
detail="User code not found or expired"
)

# Mark device as authorized with tokens
success = self.server_instance.auth_connector.set_device_code_authorized(
device_code,
user_id,
firebase_id_token,
firebase_refresh_token
)

if not success:
raise HTTPException(
status_code=500,
detail="Failed to authorize device"
)

logger.info(f"[Device Authorize] Device authorized for user_code: {user_code}, user: {user_id}")
return {"success": True}

except HTTPException:
raise
except Exception as e:
logger.error(f"[Device Authorize] Error authorizing device: {e}")
raise HTTPException(status_code=500, detail=str(e))
31 changes: 31 additions & 0 deletions backend/auth/auth_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Auth service for device flow authentication.
"""

import json
import os
import firebase_admin
from firebase_admin import credentials, auth
import logging
from typing import Optional, Dict, Any
from datetime import datetime, timezone, timedelta
Expand Down Expand Up @@ -241,3 +245,30 @@ def delete_device_code(self, device_code: str) -> bool:
except Exception as e:
logger.error(f"Error deleting device code: {e}")
return False

def verify_firebase_token(self, id_token: str) -> Optional[Dict[str, Any]]:
"""Verify Firebase ID token from website/plugin."""
try:
# Initialize Firebase Admin SDK only once
if not firebase_admin._apps:
firebase_admin_json = json.loads(os.environ["FIREBASE_ADMIN_KEY"])
if not firebase_admin_json:
logger.warning("Firebase credentials not found in secrets.")
return None
cred = credentials.Certificate(firebase_admin_json)
firebase_admin.initialize_app(cred)

# Verify the ID token
decoded_token = auth.verify_id_token(id_token)
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
except Exception as e:
logger.error(f"Error verifying Firebase token: {e}")
return None

3 changes: 2 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ dependencies = [
"transformers",
"scenedetect",
"boto3",
"torchvision"
"torchvision",
"firebase-admin>=7.1.0",
]

[project.scripts]
Expand Down
13 changes: 13 additions & 0 deletions backend/services/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ def _initialize_connectors(self):
logger.info(f"[{self.__class__.__name__}] Starting up in '{env}' environment")
self.start_time = datetime.now(timezone.utc)

# Initialize Firebase Admin SDK (required for token verification)
try:
import firebase_admin
if not firebase_admin._apps:
import json
firebase_admin_json = json.loads(get_env_var("FIREBASE_ADMIN_KEY"))
from firebase_admin import credentials
cred = credentials.Certificate(firebase_admin_json)
firebase_admin.initialize_app(cred)
logger.info(f"[{self.__class__.__name__}] Firebase Admin SDK initialized")
except Exception as e:
logger.warning(f"[{self.__class__.__name__}] Firebase initialization failed: {e}")

# Get environment variables
PINECONE_API_KEY = get_env_var("PINECONE_API_KEY")
R2_ACCOUNT_ID = get_env_var("R2_ACCOUNT_ID")
Expand Down
Loading
Loading