mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
fix(dashboard): proxy modal stops pre-filling new scopes with an unrelated proxy (#4312)
The proxy assignments list returned by /api/settings/proxies/assignments is global, so its first entry belongs to some other scope. ProxyConfigModal picked `items.find(matchingScope) || items[0]`, so opening the proxy config for a freshly created provider/key (which has no assignment of its own) fell back to items[0] and pre-filled host/port/user/password from an unrelated proxy plus set hasOwnProxy=true — users reported a new provider already carried a proxy they never configured. Extracted the scope helpers into proxyAssignment.ts and added selectScopeAssignment which returns null (never items[0]) when the current scope has no assignment. The modal then shows the empty/custom state for new scopes. Both call sites now use it. TDD: src/shared/components/proxyAssignment.test.tsx (no-match -> null red->green for provider/key/global scopes; matching-scope + empty-list regression guards). Existing ProxyConfigModal component test stays green.
This commit is contained in:
committed by
GitHub
parent
466d3cf6eb
commit
5193a595bf
@@ -4,6 +4,12 @@ import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
import {
|
||||
type ProxyAssignmentItem,
|
||||
normalizeScopeId,
|
||||
isSameScopeAssignment,
|
||||
selectScopeAssignment,
|
||||
} from "./proxyAssignment";
|
||||
|
||||
const ALL_PROXY_TYPES = [
|
||||
{ value: "http", label: "HTTP" },
|
||||
@@ -33,12 +39,6 @@ type ProxyRegistryItem = {
|
||||
source?: string | null;
|
||||
};
|
||||
|
||||
type ProxyAssignmentItem = {
|
||||
proxyId?: string | null;
|
||||
scope?: string | null;
|
||||
scopeId?: string | null;
|
||||
};
|
||||
|
||||
type ProxyConfigModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -59,20 +59,6 @@ function getAssignmentScopeId(level: ProxyConfigLevel, levelId?: string) {
|
||||
return level === "global" ? null : levelId || null;
|
||||
}
|
||||
|
||||
function normalizeScopeId(scopeId?: string | null) {
|
||||
return !scopeId || scopeId === "__global__" ? null : scopeId;
|
||||
}
|
||||
|
||||
function isSameScopeAssignment(
|
||||
assignment: ProxyAssignmentItem,
|
||||
scope: string,
|
||||
scopeId: string | null
|
||||
) {
|
||||
return (
|
||||
assignment.scope === scope && normalizeScopeId(assignment.scopeId) === normalizeScopeId(scopeId)
|
||||
);
|
||||
}
|
||||
|
||||
function getCustomProxyName(level: ProxyConfigLevel, levelId?: string, levelLabel?: string) {
|
||||
const label = levelLabel || levelId || "";
|
||||
const suffix = label ? ` (${label})` : "";
|
||||
@@ -95,7 +81,7 @@ async function fetchAssignmentForScope(scope: string, scopeId: string | null) {
|
||||
|
||||
const payload = await readJson(res);
|
||||
const items: ProxyAssignmentItem[] = Array.isArray(payload?.items) ? payload.items : [];
|
||||
return items.find((item) => isSameScopeAssignment(item, scope, scopeId)) || items[0] || null;
|
||||
return selectScopeAssignment(items, scope, scopeId);
|
||||
}
|
||||
|
||||
async function fetchRegistryProxy(proxyId: string, cachedProxies: ProxyRegistryItem[]) {
|
||||
@@ -198,8 +184,7 @@ export default function ProxyConfigModal({
|
||||
if (assignmentRes.ok) {
|
||||
const assignmentPayload = await assignmentRes.json();
|
||||
const items = Array.isArray(assignmentPayload?.items) ? assignmentPayload.items : [];
|
||||
const target =
|
||||
items.find((item) => isSameScopeAssignment(item, scope, scopeId)) || items[0];
|
||||
const target = selectScopeAssignment(items, scope, scopeId);
|
||||
if (target?.proxyId) {
|
||||
setSelectedProxyId(target.proxyId);
|
||||
setHasOwnProxy(true);
|
||||
|
||||
41
src/shared/components/proxyAssignment.test.tsx
Normal file
41
src/shared/components/proxyAssignment.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { selectScopeAssignment } from "./proxyAssignment";
|
||||
|
||||
// Regression guard for the escalated bug "when I create a new provider it already
|
||||
// comes with a proxy I never configured". The proxy assignments list is global, so
|
||||
// its first entry belongs to some other scope. selectScopeAssignment must return null
|
||||
// when the current scope has no assignment of its own — never fall back to items[0],
|
||||
// which used to pre-fill freshly created providers/keys with an unrelated proxy.
|
||||
const ASSIGNMENTS = [
|
||||
{ proxyId: "socks5-acct", scope: "key", scopeId: "3c0031c1-existing-account" },
|
||||
{ proxyId: "http-claude", scope: "provider", scopeId: "claude" },
|
||||
];
|
||||
|
||||
describe("selectScopeAssignment", () => {
|
||||
it("returns null (not items[0]) for a scope with no assignment of its own", () => {
|
||||
// A brand-new provider with no proxy assignment.
|
||||
const result = selectScopeAssignment(ASSIGNMENTS, "provider", "newprovider");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a new account/key scope even when other proxies exist", () => {
|
||||
const result = selectScopeAssignment(ASSIGNMENTS, "key", "brand-new-key");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the matching assignment when the scope owns one", () => {
|
||||
const result = selectScopeAssignment(ASSIGNMENTS, "provider", "claude");
|
||||
expect(result?.proxyId).toBe("http-claude");
|
||||
});
|
||||
|
||||
it("treats the global scope (null scopeId) distinctly", () => {
|
||||
const withGlobal = [{ proxyId: "g", scope: "global", scopeId: null }, ...ASSIGNMENTS];
|
||||
expect(selectScopeAssignment(withGlobal, "global", null)?.proxyId).toBe("g");
|
||||
// a provider scope still must not borrow the global assignment
|
||||
expect(selectScopeAssignment(withGlobal, "provider", "unconfigured")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for an empty assignments list", () => {
|
||||
expect(selectScopeAssignment([], "provider", "anything")).toBeNull();
|
||||
});
|
||||
});
|
||||
40
src/shared/components/proxyAssignment.ts
Normal file
40
src/shared/components/proxyAssignment.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Proxy-assignment scope helpers, extracted from ProxyConfigModal so the pure
|
||||
* selection logic can be unit-tested without rendering the React component.
|
||||
*/
|
||||
|
||||
export type ProxyAssignmentItem = {
|
||||
proxyId?: string | null;
|
||||
scope?: string | null;
|
||||
scopeId?: string | null;
|
||||
};
|
||||
|
||||
export function normalizeScopeId(scopeId?: string | null) {
|
||||
return !scopeId || scopeId === "__global__" ? null : scopeId;
|
||||
}
|
||||
|
||||
export function isSameScopeAssignment(
|
||||
assignment: ProxyAssignmentItem,
|
||||
scope: string,
|
||||
scopeId: string | null
|
||||
) {
|
||||
return (
|
||||
assignment.scope === scope && normalizeScopeId(assignment.scopeId) === normalizeScopeId(scopeId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the proxy assignment that belongs to *this* scope, or `null` when none does.
|
||||
*
|
||||
* Must NOT fall back to `items[0]`: the assignments list is global, so the first
|
||||
* entry belongs to some other scope (e.g. another account's proxy). Returning it
|
||||
* for a scope with no assignment of its own made a freshly created provider/key
|
||||
* appear pre-filled with an unrelated proxy the user never configured. (escalated bug)
|
||||
*/
|
||||
export function selectScopeAssignment(
|
||||
items: ProxyAssignmentItem[],
|
||||
scope: string,
|
||||
scopeId: string | null
|
||||
): ProxyAssignmentItem | null {
|
||||
return items.find((item) => isSameScopeAssignment(item, scope, scopeId)) ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user