Merge PR #1300: tailscale Windows fix, quota pagination, SSE abort handling

- fix(tunnel): cross-platform tailscale probes without shell redirection
- feat(usage): paginate provider limits with page size controls
- feat(providers): stop control for one-by-one connection testing
- fix(sse): close stream gracefully on abort/disconnect instead of pipe errors
- ui(quota): simplify header, always show pagination in one row

Co-authored-by: philau2512 <dplau25122002@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-05-21 12:15:07 +07:00
parent e84ab7857a
commit 6b0dc09239
8 changed files with 1183 additions and 295 deletions

View File

@@ -1,8 +1,8 @@
import { NextResponse } from "next/server";
import { getProviderConnections } from "@/lib/localDb";
import { backfillCodexEmails } from "@/lib/oauth/providers";
import { USAGE_APIKEY_PROVIDERS, USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
// Whitelist: only safe metadata fields exposed to UI
const SAFE_FIELDS = [
"id", "provider", "authType", "name", "email", "displayName",
"priority", "globalPriority", "isActive", "defaultModel",
@@ -11,7 +11,6 @@ const SAFE_FIELDS = [
"createdAt", "updatedAt",
];
// providerSpecificData fields safe to expose (non-secret config only)
const SAFE_PSD_FIELDS = [
"baseUrl", "azureEndpoint", "deployment", "apiVersion", "accountId",
"region", "projectId", "resourceUrl", "proxyPoolId",
@@ -20,9 +19,11 @@ const SAFE_PSD_FIELDS = [
"username", "firstName", "lastName", "authMethod", "authKind",
];
const DEFAULT_PAGE_SIZE = 20;
const MAX_PAGE_SIZE = 500;
function maskName(name) {
if (typeof name !== "string" || name.length <= 16) return name;
// Names like "hahask-uDUOg90..." may embed API keys — mask if looks like key
if (/[a-zA-Z0-9_-]{32,}/.test(name)) return `${name.slice(0, 8)}***`;
return name;
}
@@ -41,12 +42,83 @@ function sanitize(c) {
return safe;
}
// GET /api/providers/client - List connections for dashboard UI (whitelist only)
export async function GET() {
function isUsageEligible(connection) {
return USAGE_SUPPORTED_PROVIDERS.includes(connection.provider) && (
connection.authType === "oauth" || USAGE_APIKEY_PROVIDERS.includes(connection.provider)
);
}
function parsePositiveInt(value, fallback) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function sortConnections(connections, sort) {
const list = [...connections];
if (sort === "provider") {
return list.sort((a, b) => {
const orderA = USAGE_SUPPORTED_PROVIDERS.indexOf(a.provider);
const orderB = USAGE_SUPPORTED_PROVIDERS.indexOf(b.provider);
if (orderA !== orderB) return orderA - orderB;
return a.provider.localeCompare(b.provider);
});
}
return list.sort((a, b) => {
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
if (priorityA !== priorityB) return priorityA - priorityB;
return (a.provider || "").localeCompare(b.provider || "");
});
}
export async function GET(request) {
try {
await backfillCodexEmails();
const connections = await getProviderConnections();
return NextResponse.json({ connections: connections.map(sanitize) });
const { searchParams } = new URL(request.url);
const provider = searchParams.get("provider") || "all";
const accountStatus = searchParams.get("accountStatus") || "all";
const sort = searchParams.get("sort") || "priority";
const page = parsePositiveInt(searchParams.get("page"), 1);
const pageSize = Math.min(parsePositiveInt(searchParams.get("pageSize"), DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE);
const allConnections = await getProviderConnections();
const eligibleConnections = allConnections.filter(isUsageEligible);
const providerOptions = Array.from(new Set(eligibleConnections.map((conn) => conn.provider))).sort();
const providerFilteredConnections = eligibleConnections.filter((conn) => (
provider === "all" || conn.provider === provider
));
const accountFilteredConnections = providerFilteredConnections.filter((conn) => {
if (accountStatus === "active") return conn.isActive ?? true;
if (accountStatus === "inactive") return !(conn.isActive ?? true);
return true;
});
const sortedConnections = sortConnections(accountFilteredConnections, sort);
const total = sortedConnections.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const offset = (currentPage - 1) * pageSize;
const pageConnections = sortedConnections.slice(offset, offset + pageSize).map(sanitize);
return NextResponse.json({
connections: pageConnections,
providerOptions,
pagination: {
page: currentPage,
pageSize,
total,
totalPages,
},
totals: {
eligibleConnections: eligibleConnections.length,
providerFilteredConnections: providerFilteredConnections.length,
},
});
} catch (error) {
console.log("Error fetching providers for client:", error);
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });