From a4094f13cfd181115f73405ec5f46490d835b697 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sat, 4 Apr 2026 22:51:38 -0400 Subject: [PATCH] Update backend/main.py --- backend/main.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/backend/main.py b/backend/main.py index 58b9a09..73c6a23 100644 --- a/backend/main.py +++ b/backend/main.py @@ -480,6 +480,120 @@ async def usage_stats() -> Dict: return usage +# Auth state stored in memory (persists while container runs) +_auth_process = None +_auth_url = None +_auth_status = "unknown" # unknown | logged_in | logged_out | pending + + +async def _check_claude_auth() -> str: + """Check if claude is authenticated by running a quick no-op""" + global _auth_status + try: + proc = await asyncio.create_subprocess_exec( + "claude", "--version", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await asyncio.wait_for(proc.communicate(), timeout=5) + # Check for saved credentials file + creds = Path("/root/.claude/.credentials.json") + if creds.exists(): + import json as _json + data = _json.loads(creds.read_text()) + if data: + _auth_status = "logged_in" + return "logged_in" + _auth_status = "logged_out" + return "logged_out" + except Exception: + _auth_status = "unknown" + return "unknown" + + +@app.get("/api/auth/status") +async def auth_status(): + """Check Claude auth status""" + global _auth_status, _auth_url + status = await _check_claude_auth() + # Try to get account info if logged in + account = None + try: + creds_file = Path("/root/.claude/.credentials.json") + if creds_file.exists(): + import json as _json + creds = _json.loads(creds_file.read_text()) + account = creds.get("account", {}).get("emailAddress") or creds.get("email") + except Exception: + pass + return { + "status": status, + "account": account, + "auth_url": _auth_url if status == "pending" else None + } + + +@app.post("/api/auth/login") +async def auth_login(): + """Initiate Claude OAuth login - returns the URL to open in browser""" + global _auth_process, _auth_url, _auth_status + _auth_status = "pending" + _auth_url = None + + try: + # Run claude auth login and capture the URL it prints + proc = await asyncio.create_subprocess_exec( + "claude", "auth", "login", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + stdin=asyncio.subprocess.DEVNULL + ) + _auth_process = proc + + # Read output lines looking for the OAuth URL (30s timeout) + url = None + try: + async def _read_url(): + while True: + line = await proc.stdout.readline() + if not line: + break + text = line.decode(errors="replace") + logger.info(f"claude auth: {text.strip()}") + # Claude prints something like: "Open this URL: https://..." + for part in text.split(): + if part.startswith("https://claude.ai") or part.startswith("https://anthropic"): + return part.strip() + return None + url = await asyncio.wait_for(_read_url(), timeout=30) + except asyncio.TimeoutError: + pass + + if url: + _auth_url = url + return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in"} + else: + return {"status": "error", "message": "Could not extract login URL from claude CLI. Try running 'claude auth login' manually in the container."} + + except Exception as e: + return {"status": "error", "message": str(e)} + + +@app.post("/api/auth/logout") +async def auth_logout(): + """Log out of Claude""" + global _auth_status + try: + proc = await asyncio.create_subprocess_exec( + "claude", "auth", "logout", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await asyncio.wait_for(proc.communicate(), timeout=10) + _auth_status = "logged_out" + return {"status": "logged_out"} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Serve static frontend app.mount("/", StaticFiles(directory="/app/frontend/dist", html=True), name="static")