mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix: improve auth consistency, billing monitoring, and client-side navigation
- Use Security(auth.requires_user) on /usage endpoint for consistency - Add BILLING_LEAK: prefix and TODO for atomic credit charging - Cache credit_db() call to avoid redundant initialization - Add test for unauthenticated /usage request returning 401 - Use router.push() instead of window.location.href for SPA navigation - Align pytest.mark.asyncio loop_scope with existing test patterns
This commit is contained in:
@@ -413,17 +413,17 @@ async def get_session(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/usage")
|
||||
@router.get(
|
||||
"/usage",
|
||||
dependencies=[Security(auth.requires_user)],
|
||||
)
|
||||
async def get_copilot_usage(
|
||||
user_id: Annotated[str | None, Depends(auth.get_user_id)],
|
||||
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||
) -> CoPilotUsageStatus:
|
||||
"""Get CoPilot usage status for the authenticated user.
|
||||
|
||||
Returns current token usage vs limits for daily and weekly windows.
|
||||
"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
return await get_usage_status(
|
||||
user_id=user_id,
|
||||
daily_token_limit=config.daily_token_limit,
|
||||
|
||||
@@ -320,6 +320,17 @@ def test_usage_uses_config_limits(
|
||||
)
|
||||
|
||||
|
||||
def test_usage_rejects_unauthenticated_request() -> None:
|
||||
"""GET /usage should return 401 when no valid JWT is provided."""
|
||||
unauthenticated_app = fastapi.FastAPI()
|
||||
unauthenticated_app.include_router(chat_routes.router)
|
||||
unauthenticated_client = fastapi.testclient.TestClient(unauthenticated_app)
|
||||
|
||||
response = unauthenticated_client.get("/usage")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# ─── Suggested prompts endpoint ──────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -118,10 +118,14 @@ async def execute_block(
|
||||
coerce_inputs_to_schema(input_data, block.input_schema)
|
||||
|
||||
# Pre-execution credit check
|
||||
# TODO: Replace this check-then-act pattern with an atomic
|
||||
# reserve-and-charge mechanism to eliminate the TOCTOU race
|
||||
# between the balance check and the post-execution charge.
|
||||
cost, cost_filter = block_usage_cost(block, input_data)
|
||||
has_cost = cost > 0
|
||||
credits = credit_db()
|
||||
if has_cost:
|
||||
balance = await credit_db().get_credits(user_id)
|
||||
balance = await credits.get_credits(user_id)
|
||||
if balance < cost:
|
||||
return ErrorResponse(
|
||||
message=(
|
||||
@@ -142,7 +146,7 @@ async def execute_block(
|
||||
# Charge credits for block execution
|
||||
if has_cost:
|
||||
try:
|
||||
await credit_db().spend_credits(
|
||||
await credits.spend_credits(
|
||||
user_id=user_id,
|
||||
cost=cost,
|
||||
metadata=UsageTransactionMetadata(
|
||||
@@ -159,11 +163,14 @@ async def execute_block(
|
||||
except InsufficientBalanceError:
|
||||
# Concurrent spend drained balance after our pre-check passed.
|
||||
# Block already executed (with possible side effects), so return
|
||||
# its output. Log the billing leak amount for reconciliation.
|
||||
# its output but log the billing leak for monitoring.
|
||||
logger.warning(
|
||||
"Post-exec credit charge failed for block %s (cost=%d)",
|
||||
"BILLING_LEAK: Post-exec credit charge failed for block %s "
|
||||
"(cost=%d, user=%s, node_exec=%s)",
|
||||
block.name,
|
||||
cost,
|
||||
user_id[:8],
|
||||
node_exec_id,
|
||||
)
|
||||
|
||||
return BlockOutputResponse(
|
||||
|
||||
@@ -64,7 +64,7 @@ def _patch_credit_db(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
class TestExecuteBlockCreditCharging:
|
||||
async def test_charges_credits_when_cost_is_positive(self):
|
||||
"""Block with cost > 0 should call spend_credits after execution."""
|
||||
|
||||
@@ -83,7 +83,7 @@ export function CopilotPage() {
|
||||
handleDrawerOpenChange,
|
||||
handleSelectSession,
|
||||
handleNewChat,
|
||||
// Delete functionality
|
||||
// Delete functionality (available via ChatSidebar context menu on all viewports)
|
||||
sessionToDelete,
|
||||
isDeleting,
|
||||
handleConfirmDelete,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@/components/__legacy__/ui/table";
|
||||
|
||||
function CoPilotUsageSection() {
|
||||
const router = useRouter();
|
||||
const { data: usage, isLoading } = useUsageLimits();
|
||||
|
||||
if (isLoading || !usage) return null;
|
||||
@@ -35,10 +36,7 @@ function CoPilotUsageSection() {
|
||||
<div className="rounded-lg border border-neutral-200 p-4 dark:border-neutral-700">
|
||||
<UsagePanelContent usage={usage} showBillingLink={false} />
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => (window.location.href = "/copilot")}
|
||||
>
|
||||
<Button className="w-full" onClick={() => router.push("/copilot")}>
|
||||
Open CoPilot
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user