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:
Zamil Majdy
2026-03-14 22:26:39 +07:00
parent 6fb3b1e87b
commit b9951a3c53
6 changed files with 31 additions and 15 deletions

View File

@@ -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,

View File

@@ -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 ──────────────────────────────────────

View File

@@ -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(

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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>