Fix CPU HLS streaming (yuv420p) and opt-in middleware for fragments
- Add -pix_fmt yuv420p to multi_res_output.py libx264 path so browsers can decode CPU-encoded segments (was producing yuv444p / High 4:4:4). - Switch silent auth check and coop fragment middlewares from opt-out blocklists to opt-in: only run for GET requests with Accept: text/html. Prevents unnecessary nav-tree/auth-menu HTTP calls on every HLS segment, IPFS proxy, and API request. - Add opaque grant token verification to L1/L2 dependencies. - Migrate client CLI to device authorization flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,6 @@ from artdag_common.middleware.auth import get_user_from_cookie
|
||||
|
||||
from .config import settings
|
||||
|
||||
# Paths that should never trigger a silent auth check
|
||||
_SKIP_PREFIXES = ("/auth/", "/static/", "/api/", "/ipfs/", "/download/", "/inbox", "/health", "/internal/", "/oembed")
|
||||
_SILENT_CHECK_COOLDOWN = 300 # 5 minutes
|
||||
_DEVICE_COOKIE = "artdag_did"
|
||||
_DEVICE_COOKIE_MAX_AGE = 30 * 24 * 3600 # 30 days
|
||||
@@ -60,14 +58,15 @@ def create_app() -> FastAPI:
|
||||
async def shutdown():
|
||||
await close_db()
|
||||
|
||||
# Silent auth check — auto-login via prompt=none OAuth
|
||||
# Silent auth check — auto-login via prompt=none OAuth.
|
||||
# Only runs for browser page loads (Accept: text/html).
|
||||
# NOTE: registered BEFORE device_id so device_id is outermost (runs first)
|
||||
@app.middleware("http")
|
||||
async def silent_auth_check(request: Request, call_next):
|
||||
path = request.url.path
|
||||
accept = request.headers.get("accept", "")
|
||||
if (
|
||||
request.method != "GET"
|
||||
or any(path.startswith(p) for p in _SKIP_PREFIXES)
|
||||
or "text/html" not in accept
|
||||
or request.headers.get("hx-request") # skip HTMX
|
||||
):
|
||||
return await call_next(request)
|
||||
@@ -148,17 +147,14 @@ def create_app() -> FastAPI:
|
||||
return response
|
||||
|
||||
# Coop fragment pre-fetch — inject nav-tree, auth-menu, cart-mini into
|
||||
# request.state for full-page HTML renders. Skips HTMX, API, and
|
||||
# internal paths. Failures are silent (fragments default to "").
|
||||
_FRAG_SKIP = ("/auth/", "/api/", "/internal/", "/health", "/oembed",
|
||||
"/ipfs/", "/download/", "/inbox", "/static/")
|
||||
|
||||
# request.state for full-page HTML renders. Opt-in: only fetches for
|
||||
# browser page loads (Accept: text/html, non-HTMX GET requests).
|
||||
@app.middleware("http")
|
||||
async def coop_fragments_middleware(request: Request, call_next):
|
||||
path = request.url.path
|
||||
accept = request.headers.get("accept", "")
|
||||
if (
|
||||
request.method != "GET"
|
||||
or any(path.startswith(p) for p in _FRAG_SKIP)
|
||||
or "text/html" not in accept
|
||||
or request.headers.get("hx-request")
|
||||
or request.headers.get(fragments.FRAGMENT_HEADER)
|
||||
):
|
||||
@@ -171,7 +167,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
user = get_user_from_cookie(request)
|
||||
auth_params = {"email": user.email or user.username} if user else {}
|
||||
nav_params = {"app_name": "artdag", "path": path}
|
||||
nav_params = {"app_name": "artdag", "path": request.url.path}
|
||||
|
||||
try:
|
||||
nav_tree_html, auth_menu_html, cart_mini_html = await _fetch_frags([
|
||||
|
||||
@@ -54,6 +54,77 @@ def get_templates(request: Request) -> Environment:
|
||||
return request.app.state.templates
|
||||
|
||||
|
||||
async def _verify_opaque_grant(token: str) -> Optional[UserContext]:
|
||||
"""Verify an opaque grant token via account server, with Redis cache."""
|
||||
import httpx
|
||||
import json
|
||||
|
||||
if not settings.internal_account_url:
|
||||
return None
|
||||
|
||||
# Check L1 Redis cache first
|
||||
cache_key = f"grant_verify:{token[:16]}"
|
||||
try:
|
||||
r = get_redis_client()
|
||||
cached = r.get(cache_key)
|
||||
if cached is not None:
|
||||
if cached == "__invalid__":
|
||||
return None
|
||||
data = json.loads(cached)
|
||||
return UserContext(
|
||||
username=data["username"],
|
||||
actor_id=data["actor_id"],
|
||||
token=token,
|
||||
email=data.get("email", ""),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Call account server
|
||||
verify_url = f"{settings.internal_account_url.rstrip('/')}/auth/internal/verify-grant"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
resp = await client.get(verify_url, params={"token": token})
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
data = resp.json()
|
||||
if not data.get("valid"):
|
||||
# Cache negative result briefly
|
||||
try:
|
||||
r = get_redis_client()
|
||||
r.set(cache_key, "__invalid__", ex=60)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
username = data.get("username", "")
|
||||
display_name = data.get("display_name", "")
|
||||
actor_id = f"@{username}" if username else ""
|
||||
ctx = UserContext(
|
||||
username=username,
|
||||
actor_id=actor_id,
|
||||
token=token,
|
||||
email=username,
|
||||
)
|
||||
|
||||
# Cache positive result for 5 minutes
|
||||
try:
|
||||
r = get_redis_client()
|
||||
cache_data = json.dumps({
|
||||
"username": username,
|
||||
"actor_id": actor_id,
|
||||
"email": username,
|
||||
"display_name": display_name,
|
||||
})
|
||||
r.set(cache_key, cache_data, ex=300)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> Optional[UserContext]:
|
||||
"""
|
||||
Get the current user from request (cookie or header).
|
||||
@@ -61,11 +132,19 @@ async def get_current_user(request: Request) -> Optional[UserContext]:
|
||||
This is a permissive dependency - returns None if not authenticated.
|
||||
Use require_auth for routes that require authentication.
|
||||
"""
|
||||
# Try header first (API clients)
|
||||
# Try header first (API clients — JWT tokens)
|
||||
ctx = get_user_from_header(request)
|
||||
if ctx:
|
||||
return ctx
|
||||
|
||||
# Try opaque grant token (device flow / CLI tokens)
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
ctx = await _verify_opaque_grant(token)
|
||||
if ctx:
|
||||
return ctx
|
||||
|
||||
# Fall back to cookie (browser)
|
||||
return get_user_from_cookie(request)
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ class MultiResolutionHLSOutput:
|
||||
"-bufsize", f"{quality.bitrate * 2}k",
|
||||
])
|
||||
cmd.extend([
|
||||
"-pix_fmt", "yuv420p", # Required for browser MSE compatibility
|
||||
"-g", str(int(self.fps * self.segment_duration)), # Keyframe interval = segment duration
|
||||
"-keyint_min", str(int(self.fps * self.segment_duration)),
|
||||
"-sc_threshold", "0", # Disable scene change detection for consistent segments
|
||||
|
||||
Reference in New Issue
Block a user