Commit
·
bbc9c59
1
Parent(s):
2262841
feat: add Firebase Auth ID token verification + /api/me endpoint; support static password + Firebase ID tokens
Browse files- api_server.py +63 -19
- firebase_app_check.py +25 -1
api_server.py
CHANGED
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
| 15 |
from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
|
| 16 |
import logging
|
| 17 |
from logging.handlers import BufferingHandler
|
| 18 |
-
from firebase_app_check import verify_app_check_token
|
| 19 |
|
| 20 |
# Import face swap functionality
|
| 21 |
import sys
|
|
@@ -31,14 +31,29 @@ API_PASSWORD = os.getenv("API_PASSWORD", "logicgo_videoswap@153")
|
|
| 31 |
security = HTTPBearer()
|
| 32 |
|
| 33 |
def verify_api_key(credentials: HTTPAuthorizationCredentials = Security(security)):
|
| 34 |
-
"""Verify API key from Bearer token
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# CORS middleware
|
| 44 |
app.add_middleware(
|
|
@@ -340,7 +355,7 @@ async def process_face_swap(job_id: str, source_image_path: str, target_video_pa
|
|
| 340 |
@app.post("/api/source-image", response_model=SourceImageResponse)
|
| 341 |
async def upload_source_image(
|
| 342 |
file: UploadFile = File(...),
|
| 343 |
-
api_key:
|
| 344 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 345 |
):
|
| 346 |
"""Upload and store source image in MongoDB"""
|
|
@@ -377,7 +392,7 @@ async def upload_source_image(
|
|
| 377 |
@app.post("/api/target-video", response_model=TargetVideoResponse)
|
| 378 |
async def upload_target_video(
|
| 379 |
file: UploadFile = File(...),
|
| 380 |
-
api_key:
|
| 381 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 382 |
):
|
| 383 |
"""Upload and store target video in MongoDB"""
|
|
@@ -415,7 +430,7 @@ async def upload_target_video(
|
|
| 415 |
async def start_face_swap(
|
| 416 |
request: FaceSwapRequest,
|
| 417 |
background_tasks: BackgroundTasks,
|
| 418 |
-
api_key:
|
| 419 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 420 |
):
|
| 421 |
"""Start face swap processing"""
|
|
@@ -460,7 +475,7 @@ async def start_face_swap(
|
|
| 460 |
raise HTTPException(status_code=500, detail=f"Error starting face swap: {str(e)}")
|
| 461 |
|
| 462 |
@app.get("/api/job/{job_id}", response_model=JobStatus)
|
| 463 |
-
async def get_job_status(job_id: str, api_key:
|
| 464 |
"""Get job status"""
|
| 465 |
job = await jobs_collection.find_one({"job_id": job_id})
|
| 466 |
if not job:
|
|
@@ -480,7 +495,7 @@ async def get_job_status(job_id: str, api_key: str = Depends(verify_api_key), _a
|
|
| 480 |
)
|
| 481 |
|
| 482 |
@app.get("/api/result-video/{result_video_id}")
|
| 483 |
-
async def get_result_video(result_video_id: str, api_key:
|
| 484 |
"""Get result video file"""
|
| 485 |
result = await result_videos_collection.find_one({"_id": ObjectId(result_video_id)})
|
| 486 |
if not result:
|
|
@@ -496,7 +511,7 @@ async def get_result_video(result_video_id: str, api_key: str = Depends(verify_a
|
|
| 496 |
)
|
| 497 |
|
| 498 |
@app.get("/api/source-images", response_model=List[SourceImageResponse])
|
| 499 |
-
async def list_source_images(api_key:
|
| 500 |
"""List all source images"""
|
| 501 |
cursor = source_images_collection.find().sort("uploaded_at", -1)
|
| 502 |
images = []
|
|
@@ -511,7 +526,7 @@ async def list_source_images(api_key: str = Depends(verify_api_key), _app_check_
|
|
| 511 |
return images
|
| 512 |
|
| 513 |
@app.get("/api/target-videos", response_model=List[TargetVideoResponse])
|
| 514 |
-
async def list_target_videos(api_key:
|
| 515 |
"""List all target videos"""
|
| 516 |
cursor = target_videos_collection.find().sort("uploaded_at", -1)
|
| 517 |
videos = []
|
|
@@ -526,7 +541,7 @@ async def list_target_videos(api_key: str = Depends(verify_api_key), _app_check_
|
|
| 526 |
return videos
|
| 527 |
|
| 528 |
@app.get("/api/result-videos", response_model=List[ResultVideoResponse])
|
| 529 |
-
async def list_result_videos(api_key:
|
| 530 |
"""List all result videos"""
|
| 531 |
cursor = result_videos_collection.find().sort("created_at", -1)
|
| 532 |
results = []
|
|
@@ -543,7 +558,7 @@ async def list_result_videos(api_key: str = Depends(verify_app_check), _app_chec
|
|
| 543 |
return results
|
| 544 |
|
| 545 |
@app.get("/api/health")
|
| 546 |
-
async def api_health(api_key:
|
| 547 |
"""Health check endpoint with GPU status (requires authentication)"""
|
| 548 |
import onnxruntime
|
| 549 |
available_providers = onnxruntime.get_available_providers()
|
|
@@ -589,7 +604,7 @@ async def get_api_logs(
|
|
| 589 |
limit: int = 100,
|
| 590 |
level: Optional[str] = None,
|
| 591 |
endpoint: Optional[str] = None,
|
| 592 |
-
api_key:
|
| 593 |
):
|
| 594 |
"""Get API logs from MongoDB"""
|
| 595 |
if api_logs_collection is None:
|
|
@@ -623,6 +638,35 @@ async def get_api_logs(
|
|
| 623 |
except Exception as e:
|
| 624 |
raise HTTPException(status_code=500, detail=f"Error fetching logs: {str(e)}")
|
| 625 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
if __name__ == "__main__":
|
| 627 |
import uvicorn
|
| 628 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 15 |
from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
|
| 16 |
import logging
|
| 17 |
from logging.handlers import BufferingHandler
|
| 18 |
+
from firebase_app_check import verify_app_check_token, verify_firebase_id_token
|
| 19 |
|
| 20 |
# Import face swap functionality
|
| 21 |
import sys
|
|
|
|
| 31 |
security = HTTPBearer()
|
| 32 |
|
| 33 |
def verify_api_key(credentials: HTTPAuthorizationCredentials = Security(security)):
|
| 34 |
+
"""Verify API key from Bearer token.
|
| 35 |
+
|
| 36 |
+
Supports two authentication methods:
|
| 37 |
+
1. Static password (API_PASSWORD)
|
| 38 |
+
2. Firebase ID token (from Firebase Auth)
|
| 39 |
+
"""
|
| 40 |
+
token = credentials.credentials
|
| 41 |
+
|
| 42 |
+
# Try static password first
|
| 43 |
+
if token == API_PASSWORD:
|
| 44 |
+
return {"auth_mode": "static", "token": token}
|
| 45 |
+
|
| 46 |
+
# Try Firebase ID token
|
| 47 |
+
firebase_claims = verify_firebase_id_token(token)
|
| 48 |
+
if firebase_claims:
|
| 49 |
+
return {"auth_mode": "firebase", "claims": firebase_claims}
|
| 50 |
+
|
| 51 |
+
# If neither works, raise error
|
| 52 |
+
raise HTTPException(
|
| 53 |
+
status_code=401,
|
| 54 |
+
detail="Invalid authentication credentials. Use static password or Firebase ID token.",
|
| 55 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 56 |
+
)
|
| 57 |
|
| 58 |
# CORS middleware
|
| 59 |
app.add_middleware(
|
|
|
|
| 355 |
@app.post("/api/source-image", response_model=SourceImageResponse)
|
| 356 |
async def upload_source_image(
|
| 357 |
file: UploadFile = File(...),
|
| 358 |
+
api_key: dict = Depends(verify_api_key),
|
| 359 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 360 |
):
|
| 361 |
"""Upload and store source image in MongoDB"""
|
|
|
|
| 392 |
@app.post("/api/target-video", response_model=TargetVideoResponse)
|
| 393 |
async def upload_target_video(
|
| 394 |
file: UploadFile = File(...),
|
| 395 |
+
api_key: dict = Depends(verify_api_key),
|
| 396 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 397 |
):
|
| 398 |
"""Upload and store target video in MongoDB"""
|
|
|
|
| 430 |
async def start_face_swap(
|
| 431 |
request: FaceSwapRequest,
|
| 432 |
background_tasks: BackgroundTasks,
|
| 433 |
+
api_key: dict = Depends(verify_api_key),
|
| 434 |
_app_check_ok: bool = Depends(verify_app_check)
|
| 435 |
):
|
| 436 |
"""Start face swap processing"""
|
|
|
|
| 475 |
raise HTTPException(status_code=500, detail=f"Error starting face swap: {str(e)}")
|
| 476 |
|
| 477 |
@app.get("/api/job/{job_id}", response_model=JobStatus)
|
| 478 |
+
async def get_job_status(job_id: str, api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 479 |
"""Get job status"""
|
| 480 |
job = await jobs_collection.find_one({"job_id": job_id})
|
| 481 |
if not job:
|
|
|
|
| 495 |
)
|
| 496 |
|
| 497 |
@app.get("/api/result-video/{result_video_id}")
|
| 498 |
+
async def get_result_video(result_video_id: str, api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 499 |
"""Get result video file"""
|
| 500 |
result = await result_videos_collection.find_one({"_id": ObjectId(result_video_id)})
|
| 501 |
if not result:
|
|
|
|
| 511 |
)
|
| 512 |
|
| 513 |
@app.get("/api/source-images", response_model=List[SourceImageResponse])
|
| 514 |
+
async def list_source_images(api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 515 |
"""List all source images"""
|
| 516 |
cursor = source_images_collection.find().sort("uploaded_at", -1)
|
| 517 |
images = []
|
|
|
|
| 526 |
return images
|
| 527 |
|
| 528 |
@app.get("/api/target-videos", response_model=List[TargetVideoResponse])
|
| 529 |
+
async def list_target_videos(api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 530 |
"""List all target videos"""
|
| 531 |
cursor = target_videos_collection.find().sort("uploaded_at", -1)
|
| 532 |
videos = []
|
|
|
|
| 541 |
return videos
|
| 542 |
|
| 543 |
@app.get("/api/result-videos", response_model=List[ResultVideoResponse])
|
| 544 |
+
async def list_result_videos(api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 545 |
"""List all result videos"""
|
| 546 |
cursor = result_videos_collection.find().sort("created_at", -1)
|
| 547 |
results = []
|
|
|
|
| 558 |
return results
|
| 559 |
|
| 560 |
@app.get("/api/health")
|
| 561 |
+
async def api_health(api_key: dict = Depends(verify_api_key), _app_check_ok: bool = Depends(verify_app_check)):
|
| 562 |
"""Health check endpoint with GPU status (requires authentication)"""
|
| 563 |
import onnxruntime
|
| 564 |
available_providers = onnxruntime.get_available_providers()
|
|
|
|
| 604 |
limit: int = 100,
|
| 605 |
level: Optional[str] = None,
|
| 606 |
endpoint: Optional[str] = None,
|
| 607 |
+
api_key: dict = Depends(verify_api_key)
|
| 608 |
):
|
| 609 |
"""Get API logs from MongoDB"""
|
| 610 |
if api_logs_collection is None:
|
|
|
|
| 638 |
except Exception as e:
|
| 639 |
raise HTTPException(status_code=500, detail=f"Error fetching logs: {str(e)}")
|
| 640 |
|
| 641 |
+
# User info endpoint
|
| 642 |
+
@app.get("/api/me")
|
| 643 |
+
async def get_current_user(api_key: dict = Depends(verify_api_key)):
|
| 644 |
+
"""Get current authenticated user info"""
|
| 645 |
+
auth_mode = api_key.get("auth_mode")
|
| 646 |
+
|
| 647 |
+
if auth_mode == "firebase":
|
| 648 |
+
claims = api_key.get("claims", {})
|
| 649 |
+
return {
|
| 650 |
+
"auth_mode": "firebase",
|
| 651 |
+
"user_id": claims.get("uid"),
|
| 652 |
+
"email": claims.get("email"),
|
| 653 |
+
"email_verified": claims.get("email_verified", False),
|
| 654 |
+
"name": claims.get("name"),
|
| 655 |
+
"picture": claims.get("picture"),
|
| 656 |
+
"firebase": {
|
| 657 |
+
"sign_in_provider": claims.get("firebase", {}).get("sign_in_provider"),
|
| 658 |
+
"iss": claims.get("iss"),
|
| 659 |
+
"aud": claims.get("aud"),
|
| 660 |
+
"auth_time": claims.get("auth_time"),
|
| 661 |
+
"exp": claims.get("exp")
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
else:
|
| 665 |
+
return {
|
| 666 |
+
"auth_mode": "static",
|
| 667 |
+
"message": "Using static API password authentication"
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
if __name__ == "__main__":
|
| 671 |
import uvicorn
|
| 672 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
firebase_app_check.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
| 4 |
|
| 5 |
try:
|
| 6 |
import firebase_admin
|
| 7 |
-
from firebase_admin import credentials
|
| 8 |
# App Check verification is available in newer firebase_admin versions
|
| 9 |
try:
|
| 10 |
from firebase_admin import app_check
|
|
@@ -13,6 +13,7 @@ try:
|
|
| 13 |
except ImportError:
|
| 14 |
firebase_admin = None
|
| 15 |
credentials = None
|
|
|
|
| 16 |
app_check = None
|
| 17 |
|
| 18 |
_initialized = False
|
|
@@ -94,4 +95,27 @@ def verify_app_check_token(token: Optional[str]) -> bool:
|
|
| 94 |
print(f"Warning: App Check token verification failed: {e}")
|
| 95 |
return False
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
|
|
|
| 4 |
|
| 5 |
try:
|
| 6 |
import firebase_admin
|
| 7 |
+
from firebase_admin import credentials, auth
|
| 8 |
# App Check verification is available in newer firebase_admin versions
|
| 9 |
try:
|
| 10 |
from firebase_admin import app_check
|
|
|
|
| 13 |
except ImportError:
|
| 14 |
firebase_admin = None
|
| 15 |
credentials = None
|
| 16 |
+
auth = None
|
| 17 |
app_check = None
|
| 18 |
|
| 19 |
_initialized = False
|
|
|
|
| 95 |
print(f"Warning: App Check token verification failed: {e}")
|
| 96 |
return False
|
| 97 |
|
| 98 |
+
def verify_firebase_id_token(id_token: Optional[str]) -> Optional[dict]:
|
| 99 |
+
"""Verify Firebase ID token and return decoded claims.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
dict: Decoded token claims (user info) if valid, None if invalid/not available
|
| 103 |
+
"""
|
| 104 |
+
if not id_token:
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
if auth is None:
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
if not initialize_firebase():
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
# Verify the ID token
|
| 115 |
+
decoded_token = auth.verify_id_token(id_token)
|
| 116 |
+
return decoded_token
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"Warning: Firebase ID token verification failed: {e}")
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
|