Files changed (1) hide show
  1. app.py +43 -1
app.py CHANGED
@@ -26,7 +26,7 @@ from dataclasses import dataclass, field
26
  from typing import Any, Optional
27
 
28
  from fastapi import Depends, FastAPI, Header, HTTPException, Request
29
- from fastapi.responses import HTMLResponse
30
  from huggingface_hub import attach_huggingface_oauth, parse_huggingface_oauth, whoami
31
  from pydantic import BaseModel
32
 
@@ -224,6 +224,48 @@ async def dispatch(req: DispatchReq, request: Request) -> dict[str, Any]:
224
  return {"status": "queued", "command_id": cmd.id}
225
 
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  # === UI ======================================================================
228
  @app.get("/")
229
  async def index() -> HTMLResponse:
 
26
  from typing import Any, Optional
27
 
28
  from fastapi import Depends, FastAPI, Header, HTTPException, Request
29
+ from fastapi.responses import HTMLResponse, RedirectResponse
30
  from huggingface_hub import attach_huggingface_oauth, parse_huggingface_oauth, whoami
31
  from pydantic import BaseModel
32
 
 
224
  return {"status": "queued", "command_id": cmd.id}
225
 
226
 
227
+ # === OAuth relay for grabette devices ========================================
228
+ @app.get("/oauth/grabette/callback")
229
+ async def grabette_oauth_relay(
230
+ code: str | None = None,
231
+ state: str | None = None,
232
+ error: str | None = None,
233
+ error_description: str | None = None,
234
+ ) -> RedirectResponse | HTMLResponse:
235
+ """Relay the HF OAuth callback to the originating grabette on the local network.
236
+
237
+ The grabette encodes its mDNS hostname into the OAuth state as
238
+ ``{hostname}|{session_id}``. This endpoint splits that apart and issues a
239
+ 302 redirect so the user's browser (on the same LAN as the grabette) reaches
240
+ ``http://{hostname}.local:8000/api/hf-auth/oauth/callback``.
241
+
242
+ Only this Space URL needs to be registered as a redirect_uri in the HF
243
+ OAuth app — one entry covers every grabette regardless of hostname.
244
+ """
245
+ if not state or "|" not in state:
246
+ return HTMLResponse("Missing or invalid state parameter.", status_code=400)
247
+
248
+ hostname, session_id = state.split("|", 1)
249
+ from urllib.parse import urlencode
250
+ base = f"http://{hostname}.local:8000/api/hf-auth/oauth/callback"
251
+
252
+ if error:
253
+ # Forward the error to the grabette so it can mark the session as failed
254
+ # and stop the polling loop on the frontend.
255
+ params: dict = {"error": error, "state": session_id}
256
+ if error_description:
257
+ params["error_description"] = error_description
258
+ return RedirectResponse(f"{base}?{urlencode(params)}", status_code=302)
259
+
260
+ if not code:
261
+ return HTMLResponse("Missing code.", status_code=400)
262
+
263
+ return RedirectResponse(
264
+ f"{base}?{urlencode({'code': code, 'state': session_id})}",
265
+ status_code=302,
266
+ )
267
+
268
+
269
  # === UI ======================================================================
270
  @app.get("/")
271
  async def index() -> HTMLResponse: