Files
ai-agent/backend/accounts/routes.py
Gabriel Vidal 0ff9e40242 refactor(backend): split main.py into domain packages with app.state injection
main.py (5146 lines, 106 routes) becomes an assembly only: one package per
domain — core, files, settings, dashboards, conversations, diff,
notifications, forms, runs, accounts, workers, models, cron, agents,
projects, services, goals, memories, plans, templates — each exposing an
APIRouter; the flat domain modules move into their package behind a barrel
that keeps the old `import conversations` / `import projects` spellings.

The shared singletons (store, meta_store, hub, indexer, …) are built once by
core.state.build_state() and attached to app.state.ai; routes take them as
the `deps: State` dependency and helpers as an explicit `deps: AppState`.
conversations/pricing.py carries the per-model rates out of the parser.

Verified: route table and OpenAPI byte-identical; 90 read endpoints
golden-diffed against the monolith on a copy of the live data (identical);
write routes smoke-tested; 66 backend tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:55:47 +02:00

107 lines
4.2 KiB
Python

"""Claude account profiles and their headless login / logout."""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import accounts as accounts_mod
import schemas
from core.http import _r
from core.state import AppState, State
from runs.sidecar_client import _sidecar_call, _sidecar_get
router = APIRouter()
@router.get("/api/accounts", responses=_r(schemas.AccountsResponse))
def list_accounts(health: bool = False):
"""The Claude account profiles (hard-coded in accounts.py) a session can
run on. ``health=1`` adds each account's login status from the sidecar
(``claude auth status`` under that config dir — who, which org, which
plan); an account the sidecar doesn't know reports ``configured: false``."""
out = accounts_mod.public()
if health:
live = _sidecar_get("/accounts") or {}
by_id = {a.get("id"): a for a in live.get("accounts") or []}
for a in out:
st = by_id.get(a["id"])
a["status"] = ({
"configured": True,
"loggedIn": bool(st.get("loggedIn")),
"email": st.get("email"),
"orgName": st.get("orgName"),
"subscriptionType": st.get("subscriptionType"),
"authMethod": st.get("authMethod"),
"error": st.get("error"),
"pendingLogin": st.get("pendingLogin"),
} if st else {"configured": False, "loggedIn": False,
"error": None if live else "sidecar unreachable"})
return {"accounts": out, "default": accounts_mod.DEFAULT}
# ── account login / logout (headless `claude auth login` via the sidecar) ─────
class AccountLoginBody(BaseModel):
email: str | None = None # pre-fills the sign-in page
class AccountLoginCodeBody(BaseModel):
code: str # the `code#state` the sign-in page shows
def _login_account(account_id: str) -> str:
a = accounts_mod.valid(account_id)
if not a:
raise HTTPException(404, {"message": f"unknown account: {account_id!r}",
"kind": "unknown"})
return a
def _account_changed(deps: AppState, account_id: str) -> None:
deps.hub.publish({"type": "accounts", "account": account_id})
@router.post("/api/accounts/{account_id}/login",
responses=_r(schemas.PendingLogin))
def account_login_start(deps: State, account_id: str, body: AccountLoginBody | None = None):
"""Start a Claude subscription login for the account: the sidecar runs
``claude auth login`` under the account's config dir and answers the
sign-in URL. Open it (any device), sign in, and post the code the page
ends on to ``/login/code``. A new start replaces a pending one."""
a = _login_account(account_id)
out = _sidecar_call(deps, f"/accounts/{a}/login",
{"email": body.email if body else None}, timeout=30)
_account_changed(deps, a)
return out
@router.post("/api/accounts/{account_id}/login/code",
responses=_r(schemas.LoginCodeResult))
def account_login_code(deps: State, account_id: str, body: AccountLoginCodeBody):
"""Finish the pending login with the pasted ``code#state``. A malformed or
foreign code is a 400 and the login stays pending (paste again); a failed
exchange ends it (start again)."""
a = _login_account(account_id)
out = _sidecar_call(deps, f"/accounts/{a}/login/code", {"code": body.code},
timeout=60)
_account_changed(deps, a)
return out
@router.delete("/api/accounts/{account_id}/login")
def account_login_cancel(deps: State, account_id: str):
a = _login_account(account_id)
out = _sidecar_call(deps, f"/accounts/{a}/login", method="DELETE")
_account_changed(deps, a)
return out
@router.post("/api/accounts/{account_id}/logout",
responses=_r(schemas.LoginCodeResult))
def account_logout(deps: State, account_id: str):
"""``claude auth logout`` for the account — new runs on it fail with an
``auth`` error until it logs back in."""
a = _login_account(account_id)
out = _sidecar_call(deps, f"/accounts/{a}/logout", {}, timeout=40)
_account_changed(deps, a)
return out