Spaces:
Running
Running
File size: 5,686 Bytes
eaad188 ccb5dad eaad188 ccb5dad eaad188 560961b eaad188 ccb5dad eaad188 ccb5dad eaad188 560961b eaad188 ccb5dad eaad188 ccb5dad eaad188 ccb5dad eaad188 ccb5dad eaad188 ccb5dad 560961b eaad188 ccb5dad eaad188 ccb5dad eaad188 ccb5dad eaad188 ccb5dad | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | import os
import base64
import secrets
import httpx
import json
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse, Response
from urllib.parse import quote, parse_qs
app = FastAPI()
LT_INTERNAL = "http://127.0.0.1:5000"
CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID")
CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET")
SPACE_HOST = os.environ.get("SPACE_HOST", "localhost")
REDIRECT_URI = f"https://{SPACE_HOST}/auth/callback"
sessions: dict[str, dict] = {}
SECURE_COOKIE = not SPACE_HOST.startswith("localhost")
# ββ OAuth: login ββββββββββββββββββββββββββββββββββββββββββ
@app.get("/auth/login")
async def auth_login():
if not CLIENT_ID:
raise HTTPException(status_code=500, detail="OAuth not configured")
state = secrets.token_urlsafe(32)
scope = quote("openid profile")
url = (
f"https://huggingface.co/oauth/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={quote(REDIRECT_URI)}"
f"&response_type=code"
f"&scope={scope}"
f"&state={state}"
)
resp = RedirectResponse(url)
resp.set_cookie("oauth_state", state, max_age=600, httponly=True, samesite="lax", secure=SECURE_COOKIE)
return resp
# ββ OAuth: callback βββββββββββββββββββββββββββββββββββββββ
@app.get("/auth/callback")
async def auth_callback(code: str, state: str, request: Request):
cookie_state = request.cookies.get("oauth_state")
if not cookie_state or cookie_state != state:
raise HTTPException(status_code=400, detail="Invalid state parameter")
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
async with httpx.AsyncClient() as client:
r = await client.post(
"https://huggingface.co/oauth/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
},
headers={"Authorization": f"Basic {basic}"},
timeout=30.0,
)
if r.status_code != 200:
raise HTTPException(status_code=400, detail=f"HF OAuth failed: {r.text}")
token_data = r.json()
session_id = secrets.token_urlsafe(32)
sessions[session_id] = token_data
resp = RedirectResponse("/")
resp.delete_cookie("oauth_state")
resp.set_cookie("lt_session", session_id, httponly=True, samesite="lax", secure=SECURE_COOKIE)
return resp
# ββ OAuth: logout βββββββββββββββββββββββββββββββββββββββββ
@app.get("/auth/logout")
async def auth_logout():
resp = RedirectResponse("/")
resp.delete_cookie("lt_session")
return resp
# ββ Helper: extract target language from request body βββββ
def get_target_lang(body: bytes, content_type: str) -> str:
if not body:
return ""
if "application/json" in content_type:
try:
data = json.loads(body)
return data.get("target", "") or data.get("t", "")
except Exception:
return ""
elif "application/x-www-form-urlencoded" in content_type:
try:
form = parse_qs(body.decode("utf-8"))
return form.get("target", [""])[0] or form.get("t", [""])[0]
except Exception:
return ""
return ""
# ββ Proxy everything to LibreTranslate ββββββββββββββββββββ
async def forward(request: Request, path: str, body: bytes):
async with httpx.AsyncClient() as client:
url = f"{LT_INTERNAL}/{path}"
if request.query_params:
url = f"{url}?{request.query_params}"
headers = {
k: v for k, v in request.headers.items()
if k.lower() not in ("host", "content-length")
}
try:
lt = await client.request(
method=request.method,
url=url,
headers=headers,
content=body,
timeout=60.0,
follow_redirects=True,
)
except Exception as e:
raise HTTPException(status_code=502, detail=f"LibreTranslate unreachable: {e}")
excluded = {"content-encoding", "transfer-encoding", "content-length"}
return Response(
content=lt.content,
status_code=lt.status_code,
headers={k: v for k, v in lt.headers.items() if k.lower() not in excluded},
)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
async def proxy(request: Request, path: str):
body = await request.body()
content_type = request.headers.get("content-type", "")
# ββ /suggest gate: auth + kabyle-only ββββββββββββββββββ
if path == "suggest" and request.method == "POST":
session = request.cookies.get("lt_session")
if not session or session not in sessions:
raise HTTPException(
status_code=401,
detail="You must be logged in with a HuggingFace account to submit suggestions. "
"Visit /auth/login first."
)
target = get_target_lang(body, content_type)
if target and target != "kab":
raise HTTPException(
status_code=403,
detail="Suggestions are only accepted for the Kabyle (kab) language."
)
return await forward(request, path, body) |