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)