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:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user