Spaces:
Sleeping
Sleeping
Commit ·
97e667f
1
Parent(s): 327b929
security: require a shared service key on /tts and /extract-resume
Browse filesBoth compute endpoints were reachable by anyone who knew the Space URL.
require_service_key checks x-service-key with hmac.compare_digest and fails
closed when SERVICE_API_KEY is unset. /health stays open for platform probes.
SERVICE_API_KEY is set on this Space and the backend already sends it.
- routers/extraction.py +3 -2
- routers/tts.py +3 -2
- services/auth.py +27 -0
routers/extraction.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
import io
|
| 2 |
-
from fastapi import APIRouter, HTTPException, File, UploadFile
|
|
|
|
| 3 |
from services.pdf_extractor import extract_docx_from_stream, extract_resume_from_stream
|
| 4 |
|
| 5 |
router = APIRouter(prefix="/extract-resume", tags=["extraction"])
|
| 6 |
|
| 7 |
|
| 8 |
-
@router.post("")
|
| 9 |
async def extract_resume(file: UploadFile = File(...)):
|
| 10 |
"""
|
| 11 |
Receives an uploaded PDF file stream, extracts and structures its text,
|
|
|
|
| 1 |
import io
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
|
| 3 |
+
from services.auth import require_service_key
|
| 4 |
from services.pdf_extractor import extract_docx_from_stream, extract_resume_from_stream
|
| 5 |
|
| 6 |
router = APIRouter(prefix="/extract-resume", tags=["extraction"])
|
| 7 |
|
| 8 |
|
| 9 |
+
@router.post("", dependencies=[Depends(require_service_key)])
|
| 10 |
async def extract_resume(file: UploadFile = File(...)):
|
| 11 |
"""
|
| 12 |
Receives an uploaded PDF file stream, extracts and structures its text,
|
routers/tts.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
from typing import Literal
|
| 2 |
|
| 3 |
-
from fastapi import APIRouter, HTTPException
|
| 4 |
from fastapi.responses import Response
|
| 5 |
from pydantic import BaseModel, Field
|
| 6 |
|
|
|
|
| 7 |
from services.tts_service import synthesize, is_available, backend_status
|
| 8 |
|
| 9 |
router = APIRouter(prefix="/tts", tags=["tts"])
|
|
@@ -23,7 +24,7 @@ def tts_health():
|
|
| 23 |
return {"available": is_available(), "backend": backend_status()}
|
| 24 |
|
| 25 |
|
| 26 |
-
@router.post("")
|
| 27 |
def tts(req: TtsRequest):
|
| 28 |
"""
|
| 29 |
Synthesize interviewer speech with Kokoro. Returns audio (WAV or Ogg/Opus)
|
|
|
|
| 1 |
from typing import Literal
|
| 2 |
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
from fastapi.responses import Response
|
| 5 |
from pydantic import BaseModel, Field
|
| 6 |
|
| 7 |
+
from services.auth import require_service_key
|
| 8 |
from services.tts_service import synthesize, is_available, backend_status
|
| 9 |
|
| 10 |
router = APIRouter(prefix="/tts", tags=["tts"])
|
|
|
|
| 24 |
return {"available": is_available(), "backend": backend_status()}
|
| 25 |
|
| 26 |
|
| 27 |
+
@router.post("", dependencies=[Depends(require_service_key)])
|
| 28 |
def tts(req: TtsRequest):
|
| 29 |
"""
|
| 30 |
Synthesize interviewer speech with Kokoro. Returns audio (WAV or Ogg/Opus)
|
services/auth.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hmac
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from fastapi import Header, HTTPException
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def require_service_key(x_service_key: str | None = Header(default=None)) -> None:
|
| 8 |
+
"""
|
| 9 |
+
Shared-secret gate for the compute endpoints (/extract-resume, /tts).
|
| 10 |
+
|
| 11 |
+
This service is only ever called by the NestJS backend, but on a public host
|
| 12 |
+
it is reachable by anyone — without this, the TTS and extraction compute is
|
| 13 |
+
free for whoever finds the URL.
|
| 14 |
+
|
| 15 |
+
Fails closed: if SERVICE_API_KEY is unset the endpoints refuse traffic
|
| 16 |
+
rather than running open, so a missing env var can't silently expose them.
|
| 17 |
+
Health checks stay unauthenticated so platform probes keep working.
|
| 18 |
+
"""
|
| 19 |
+
expected = os.getenv("SERVICE_API_KEY")
|
| 20 |
+
if not expected:
|
| 21 |
+
raise HTTPException(
|
| 22 |
+
status_code=503,
|
| 23 |
+
detail="Service authentication is not configured.",
|
| 24 |
+
)
|
| 25 |
+
# compare_digest avoids leaking the key through response timing.
|
| 26 |
+
if not x_service_key or not hmac.compare_digest(x_service_key, expected):
|
| 27 |
+
raise HTTPException(status_code=401, detail="Invalid or missing service key.")
|