2025-11-23 10:55:04 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
try: # Python 3.9+
|
|
|
|
|
|
from typing import Annotated # type: ignore[attr-defined]
|
|
|
|
|
|
except ImportError: # Python 3.8 fallback
|
|
|
|
|
|
from typing_extensions import Annotated
|
|
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
from fastapi import Depends, Header, HTTPException, status
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AuthContext:
|
|
|
|
|
|
user_id: str
|
|
|
|
|
|
access_token: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_current_user(
|
|
|
|
|
|
authorization: Annotated[Optional[str], Header(convert_underscores=False)] = None,
|
2025-12-06 16:47:17 +08:00
|
|
|
|
x_supabase_access_token: Annotated[
|
|
|
|
|
|
Optional[str], Header(convert_underscores=False, alias="x-supabase-access-token")
|
|
|
|
|
|
] = None,
|
2025-11-23 10:55:04 +08:00
|
|
|
|
) -> AuthContext:
|
|
|
|
|
|
"""
|
|
|
|
|
|
验证 Supabase JWT,stage0 直接依赖 service_role 解析 token。
|
|
|
|
|
|
生产环境应通过 API Gateway 注入 user。
|
|
|
|
|
|
"""
|
2025-12-06 16:47:17 +08:00
|
|
|
|
header_token = authorization or (f"Bearer {x_supabase_access_token}" if x_supabase_access_token else None)
|
|
|
|
|
|
if not header_token or not header_token.startswith("Bearer "):
|
2025-11-23 10:55:04 +08:00
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing bearer token")
|
|
|
|
|
|
|
2025-12-06 16:47:17 +08:00
|
|
|
|
token = header_token.replace("Bearer ", "", 1).strip()
|
2025-11-23 10:55:04 +08:00
|
|
|
|
if not token:
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Empty token")
|
2025-12-06 16:47:17 +08:00
|
|
|
|
print(f"[auth] Validating Supabase token prefix={token[:8]}")
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
auth_url = f"{settings.supabase_url.rstrip('/')}/auth/v1/user"
|
2025-12-06 16:47:17 +08:00
|
|
|
|
apikey = settings.supabase_anon_key or settings.supabase_service_role_key
|
2025-11-23 10:55:04 +08:00
|
|
|
|
headers = {
|
|
|
|
|
|
"Authorization": f"Bearer {token}",
|
2025-12-06 16:47:17 +08:00
|
|
|
|
"apikey": apikey,
|
2025-11-23 10:55:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = await client.get(auth_url, headers=headers)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
except httpx.HTTPError as exc: # pragma: no cover - 网络异常
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Supabase token") from exc
|
|
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
user_id = data.get("id") or data.get("user", {}).get("id")
|
|
|
|
|
|
if not user_id:
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Supabase token")
|
|
|
|
|
|
|
|
|
|
|
|
return AuthContext(user_id=str(user_id), access_token=token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AuthDep = Annotated[AuthContext, Depends(get_current_user)]
|