feat: add authenticated tlsync token endpoint

This commit is contained in:
kcar 2026-05-28 15:12:51 +01:00
parent 550d405935
commit 93fb35eae7
4 changed files with 117 additions and 0 deletions

View File

@ -132,6 +132,11 @@ OCR_LANG=eng
MEMORY_WARNING_THRESHOLD=80 MEMORY_WARNING_THRESHOLD=80
MEMORY_REJECT_THRESHOLD=95 MEMORY_REJECT_THRESHOLD=95
# =============================================================================
# TLSync Configuration
# =============================================================================
TLSYNC_SECRET=your-tlsync-shared-secret
# ============================================================================= # =============================================================================
# CORS Configuration # CORS Configuration
# ============================================================================= # =============================================================================

44
routers/tlsync.py Normal file
View File

@ -0,0 +1,44 @@
import os
from typing import Dict
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from modules.auth.supabase_bearer import verify_supabase_jwt_str
router = APIRouter()
security = HTTPBearer(auto_error=False)
async def require_supabase_user(
credentials: HTTPAuthorizationCredentials | None = Depends(security),
) -> Dict:
"""Validate a Supabase bearer token and return the decoded session claims."""
if credentials is None or not credentials.credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
try:
return verify_supabase_jwt_str(credentials.credentials)
except HTTPException as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=exc.detail,
headers={"WWW-Authenticate": "Bearer"},
) from exc
@router.get("/token")
async def get_tlsync_token(_claims: Dict = Depends(require_supabase_user)) -> Dict[str, str]:
"""Return the API-side TLSync shared secret for authenticated clients."""
tlsync_secret = os.getenv("TLSYNC_SECRET")
if not tlsync_secret:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="TLSync secret is not configured",
)
return {"token": tlsync_secret}

View File

@ -35,6 +35,7 @@ from routers.dev.test_analysis import router as test_analysis_router
from routers.queue_management import router as queue_management_router from routers.queue_management import router as queue_management_router
from routers.maintenance.redis_admin import router as redis_admin_router from routers.maintenance.redis_admin import router as redis_admin_router
from routers import provisioning as provisioning_router from routers import provisioning as provisioning_router
from routers import tlsync as tlsync_router
from routers.transcribe.sessions import router as sessions_router from routers.transcribe.sessions import router as sessions_router
from routers.transcribe.canvas_events import router as canvas_events_router from routers.transcribe.canvas_events import router as canvas_events_router
from routers.transcribe.keywords import router as keywords_router from routers.transcribe.keywords import router as keywords_router
@ -124,6 +125,9 @@ def register_routes(app: FastAPI):
# Provisioning Routes # Provisioning Routes
app.include_router(provisioning_router.router) app.include_router(provisioning_router.router)
# TLSync runtime auth route
app.include_router(tlsync_router.router, prefix="/tlsync", tags=["TLSync"])
# Transcription Routes (CIS Phase 1) # Transcription Routes (CIS Phase 1)
app.include_router(sessions_router, prefix="/transcribe", tags=["Transcription Sessions"]) app.include_router(sessions_router, prefix="/transcribe", tags=["Transcription Sessions"])
app.include_router(canvas_events_router, prefix="/transcribe", tags=["Transcription Canvas Events"]) app.include_router(canvas_events_router, prefix="/transcribe", tags=["Transcription Canvas Events"])

View File

@ -0,0 +1,64 @@
import importlib
import time
import jwt
from fastapi import FastAPI
from fastapi.testclient import TestClient
def _client(monkeypatch, jwt_secret="test-jwt-secret-with-at-least-32-bytes", tlsync_secret="test-tlsync-secret"):
monkeypatch.setenv("JWT_SECRET", jwt_secret)
if tlsync_secret is None:
monkeypatch.delenv("TLSYNC_SECRET", raising=False)
else:
monkeypatch.setenv("TLSYNC_SECRET", tlsync_secret)
tlsync = importlib.import_module("routers.tlsync")
app = FastAPI()
app.include_router(tlsync.router, prefix="/tlsync")
return TestClient(app), jwt_secret
def _token(jwt_secret: str) -> str:
now = int(time.time())
return jwt.encode(
{
"sub": "user-123",
"aud": "authenticated",
"iat": now,
"exp": now + 3600,
},
jwt_secret,
algorithm="HS256",
)
def test_tlsync_token_requires_bearer_auth(monkeypatch):
client, _ = _client(monkeypatch)
response = client.get("/tlsync/token")
assert response.status_code == 401
def test_tlsync_token_returns_configured_secret(monkeypatch):
client, jwt_secret = _client(monkeypatch, tlsync_secret="runtime-secret")
response = client.get(
"/tlsync/token",
headers={"Authorization": f"Bearer {_token(jwt_secret)}"},
)
assert response.status_code == 200
assert response.json() == {"token": "runtime-secret"}
def test_tlsync_token_500_when_secret_missing(monkeypatch):
client, jwt_secret = _client(monkeypatch, tlsync_secret=None)
response = client.get(
"/tlsync/token",
headers={"Authorization": f"Bearer {_token(jwt_secret)}"},
)
assert response.status_code == 500