diff --git a/backend/main.py b/backend/main.py index 80c795c..28ebc72 100644 --- a/backend/main.py +++ b/backend/main.py @@ -550,16 +550,24 @@ async def auth_status(): async def auth_login(): """Initiate Claude OAuth login - returns the URL to open in browser""" global _auth_process, _auth_url, _auth_status + + # Kill any existing auth process + if _auth_process and _auth_process.returncode is None: + try: + _auth_process.kill() + except Exception: + pass + _auth_status = "pending" _auth_url = None try: - # Run claude auth login --claudeai and capture the OAuth URL + # Run claude auth login --claudeai with stdin PIPE so we can send the code later proc = await asyncio.create_subprocess_exec( "claude", "auth", "login", "--claudeai", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, - stdin=asyncio.subprocess.DEVNULL + stdin=asyncio.subprocess.PIPE ) _auth_process = proc @@ -575,14 +583,9 @@ async def auth_login(): text = line.decode(errors="replace") collected += text logger.info(f"claude auth: {text.strip()}") - # The URL contains /oauth/authorize and starts with https:// for part in text.split(): cleaned = part.strip().rstrip("\\n") - if cleaned.startswith("https://") and "oauth" in cleaned: - return cleaned - if cleaned.startswith("https://claude.com"): - return cleaned - if cleaned.startswith("https://console.anthropic.com"): + if cleaned.startswith("https://") and ("oauth" in cleaned or "claude.com" in cleaned): return cleaned logger.info(f"Full auth output: {collected}") return None @@ -590,16 +593,17 @@ async def auth_login(): except asyncio.TimeoutError: logger.warning("Timed out waiting for auth URL") - # Don't wait for the process to finish — it blocks waiting for browser callback - # It will complete in the background when user finishes OAuth in browser - if url: _auth_url = url - return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in with Claude Max"} + return { + "status": "pending", + "auth_url": url, + "message": "Open this URL, authorize, then paste the code you receive back here." + } else: return { "status": "error", - "message": "Could not extract login URL. Try running manually: docker exec -it claude-persistent-agent claude auth login --claudeai" + "message": "Could not extract login URL. Try: docker exec -it claude-persistent-agent claude auth login --claudeai" } except Exception as e: @@ -607,6 +611,58 @@ async def auth_login(): return {"status": "error", "message": str(e)} +class AuthCode(BaseModel): + code: str + + +@app.post("/api/auth/code") +async def auth_submit_code(payload: AuthCode): + """Submit the OAuth authorization code from the browser callback""" + global _auth_process, _auth_status + + if not _auth_process or _auth_process.returncode is not None: + return {"status": "error", "message": "No active login session. Click 'Login' first."} + + try: + # Send the code to the waiting claude auth login process via stdin + code = payload.code.strip() + "\n" + _auth_process.stdin.write(code.encode()) + await _auth_process.stdin.drain() + _auth_process.stdin.close() + + # Wait for the process to finish + try: + stdout, _ = await asyncio.wait_for(_auth_process.communicate(), timeout=30) + output = stdout.decode(errors="replace") if stdout else "" + logger.info(f"Auth code result: {output}") + except asyncio.TimeoutError: + logger.warning("Auth process timed out after submitting code") + + # Check if login succeeded + check_proc = await asyncio.create_subprocess_exec( + "claude", "auth", "status", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + check_stdout, _ = await asyncio.wait_for(check_proc.communicate(), timeout=10) + check_output = check_stdout.decode(errors="replace").strip() + + try: + import json as _json + data = _json.loads(check_output) + if data.get("loggedIn"): + _auth_status = "logged_in" + return {"status": "logged_in", "message": "Successfully logged in!", "account": data.get("email")} + except Exception: + pass + + _auth_status = "logged_out" + return {"status": "error", "message": f"Login may have failed. Auth status: {check_output}"} + + except Exception as e: + logger.error(f"Auth code submission failed: {e}") + return {"status": "error", "message": str(e)} + + @app.post("/api/auth/logout") async def auth_logout(): """Log out of Claude"""