chore(duplication): share proxy route handlers (#5472)

Share proxy route handlers; add resolveProxyLookupResponse regression test (3 branches + custom whereUsed param name).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
Jan Leon
2026-06-30 03:10:43 +02:00
committed by GitHub
parent ae93cfbee7
commit 152ffc2ebf
4 changed files with 259 additions and 255 deletions

View File

@@ -1,39 +1,20 @@
import { listProxies } from "@/lib/localDb";
import {
createProxy,
createProxyAndAssign,
deleteProxyById,
getProxyById,
getProxyWhereUsed,
listProxies,
updateProxy,
updateProxyAndAssign,
} from "@/lib/localDb";
import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
handleProxyCreate,
handleProxyDelete,
handleProxyUpdate,
resolveProxyLookupResponse,
} from "@/lib/api/proxyRegistryRouteHandlers";
import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const whereUsed = searchParams.get("whereUsed") === "1";
if (id && whereUsed) {
const usage = await getProxyWhereUsed(id);
return Response.json(usage);
}
if (id) {
const proxy = await getProxyById(id, { includeSecrets: false });
if (!proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(proxy);
}
const lookupResponse = await resolveProxyLookupResponse(searchParams, "whereUsed");
if (lookupResponse) return lookupResponse;
const proxies = await listProxies({ includeSecrets: false });
// #3508: expose the SOCKS5 feature flag at runtime so the dashboard reflects the live
@@ -55,112 +36,17 @@ export async function GET(request: Request) {
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(createProxyRegistrySchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { assignment, ...proxyFields } = validation.data;
if (assignment) {
const result = await createProxyAndAssign(proxyFields, assignment);
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 });
}
const created = await createProxy(proxyFields);
return Response.json(created, { status: 201 });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to create proxy");
}
return handleProxyCreate(request);
}
export async function PATCH(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(updateProxyRegistrySchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { id, assignment, ...changes } = validation.data;
if (assignment) {
const result = await updateProxyAndAssign(id, changes, assignment);
if (!result?.proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment });
}
const updated = await updateProxy(id, changes);
if (!updated) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(updated);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to update proxy");
}
return handleProxyUpdate(request);
}
export async function DELETE(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const force = searchParams.get("force") === "1";
if (!id) {
return createErrorResponse({
status: 400,
message: "id is required",
type: "invalid_request",
});
}
const deleted = await deleteProxyById(id, { force });
if (!deleted) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json({ success: true });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to delete proxy");
}
return handleProxyDelete(request);
}

View File

@@ -1,18 +1,12 @@
import { listProxies } from "@/lib/localDb";
import {
createProxy,
createProxyAndAssign,
deleteProxyById,
getProxyById,
getProxyWhereUsed,
listProxies,
updateProxy,
updateProxyAndAssign,
} from "@/lib/localDb";
import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
handleProxyCreate,
handleProxyDelete,
handleProxyUpdate,
resolveProxyLookupResponse,
} from "@/lib/api/proxyRegistryRouteHandlers";
import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
function toPagination(searchParams: URLSearchParams) {
const limit = Math.max(1, Math.min(200, Number(searchParams.get("limit") || 50)));
@@ -26,21 +20,8 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const whereUsed = searchParams.get("where_used") === "1";
if (id && whereUsed) {
const usage = await getProxyWhereUsed(id);
return Response.json(usage);
}
if (id) {
const proxy = await getProxyById(id, { includeSecrets: false });
if (!proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(proxy);
}
const lookupResponse = await resolveProxyLookupResponse(searchParams, "where_used");
if (lookupResponse) return lookupResponse;
const { limit, offset } = toPagination(searchParams);
const items = await listProxies({ includeSecrets: false });
@@ -57,115 +38,17 @@ export async function GET(request: Request) {
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(createProxyRegistrySchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { assignment, ...proxyFields } = validation.data;
if (assignment) {
const result = await createProxyAndAssign(proxyFields, assignment);
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 });
}
const created = await createProxy(proxyFields);
return Response.json(created, { status: 201 });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to create proxy");
}
return handleProxyCreate(request);
}
export async function PATCH(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(updateProxyRegistrySchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { id, assignment, ...changes } = validation.data;
if (assignment) {
const result = await updateProxyAndAssign(id, changes, assignment);
if (!result?.proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment });
}
const updated = await updateProxy(id, changes);
if (!updated) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(updated);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to update proxy");
}
return handleProxyUpdate(request);
}
export async function DELETE(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const force = searchParams.get("force") === "1";
if (!id) {
return createErrorResponse({
status: 400,
message: "id is required",
type: "invalid_request",
});
}
const deleted = await deleteProxyById(id, { force });
if (!deleted) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json({ success: true });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to delete proxy");
}
return handleProxyDelete(request);
}

View File

@@ -0,0 +1,142 @@
import {
createProxy,
createProxyAndAssign,
deleteProxyById,
getProxyById,
getProxyWhereUsed,
updateProxy,
updateProxyAndAssign,
} from "@/lib/localDb";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
async function readJsonBody(request: Request) {
try {
return { ok: true as const, body: await request.json() };
} catch {
return {
ok: false as const,
response: createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
}),
};
}
}
export async function resolveProxyLookupResponse(
searchParams: URLSearchParams,
whereUsedParam: string
): Promise<Response | null> {
const id = searchParams.get("id");
const whereUsed = searchParams.get(whereUsedParam) === "1";
if (id && whereUsed) {
const usage = await getProxyWhereUsed(id);
return Response.json(usage);
}
if (!id) {
return null;
}
const proxy = await getProxyById(id, { includeSecrets: false });
if (!proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(proxy);
}
export async function handleProxyCreate(request: Request) {
const parsed = await readJsonBody(request);
if (!parsed.ok) return parsed.response;
try {
const validation = validateBody(createProxyRegistrySchema, parsed.body);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { assignment, ...proxyFields } = validation.data;
if (assignment) {
const result = await createProxyAndAssign(proxyFields, assignment);
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 });
}
const created = await createProxy(proxyFields);
return Response.json(created, { status: 201 });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to create proxy");
}
}
export async function handleProxyUpdate(request: Request) {
const parsed = await readJsonBody(request);
if (!parsed.ok) return parsed.response;
try {
const validation = validateBody(updateProxyRegistrySchema, parsed.body);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { id, assignment, ...changes } = validation.data;
if (assignment) {
const result = await updateProxyAndAssign(id, changes, assignment);
if (!result?.proxy) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
clearDispatcherCache();
return Response.json({ ...result.proxy, assignment: result.assignment });
}
const updated = await updateProxy(id, changes);
if (!updated) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json(updated);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to update proxy");
}
}
export async function handleProxyDelete(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const force = searchParams.get("force") === "1";
if (!id) {
return createErrorResponse({
status: 400,
message: "id is required",
type: "invalid_request",
});
}
const deleted = await deleteProxyById(id, { force });
if (!deleted) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json({ success: true });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to delete proxy");
}
}

View File

@@ -0,0 +1,93 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Regression coverage for the shared proxy-route handlers extracted in #5472.
// resolveProxyLookupResponse is the single point of truth for the GET branch of
// both /api/settings/proxies (whereUsed param: "whereUsed") and
// /api/v1/management/proxies (whereUsed param: "where_used"), so its three
// branches — id+whereUsed, id-only, list (no id) — must stay equivalent across
// the parameterized callers.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-route-handlers-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-secret";
const core = await import("../../src/lib/db/core.ts");
const proxiesDb = await import("../../src/lib/db/proxies.ts");
const { resolveProxyLookupResponse } = await import(
"../../src/lib/api/proxyRegistryRouteHandlers.ts"
);
async function resetStorage() {
delete process.env.INITIAL_PASSWORD;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("resolveProxyLookupResponse returns null for the list path (no id)", async () => {
await resetStorage();
const result = await resolveProxyLookupResponse(new URLSearchParams(), "whereUsed");
assert.equal(result, null);
});
test("resolveProxyLookupResponse returns the proxy when id matches", async () => {
await resetStorage();
const created = await proxiesDb.createProxy({
name: "Lookup Proxy",
type: "http",
host: "127.0.0.1",
port: 8080,
});
assert.ok(created?.id);
const result = await resolveProxyLookupResponse(
new URLSearchParams({ id: created.id }),
"whereUsed"
);
assert.ok(result instanceof Response);
assert.equal(result.status, 200);
const body = (await result.json()) as Record<string, unknown>;
assert.equal(body.id, created.id);
assert.equal(body.name, "Lookup Proxy");
});
test("resolveProxyLookupResponse returns 404 for an unknown id", async () => {
await resetStorage();
const result = await resolveProxyLookupResponse(
new URLSearchParams({ id: "does-not-exist" }),
"whereUsed"
);
assert.ok(result instanceof Response);
assert.equal(result.status, 404);
});
test("resolveProxyLookupResponse honors the caller's whereUsed param name (where_used)", async () => {
await resetStorage();
const created = await proxiesDb.createProxy({
name: "Usage Proxy",
type: "http",
host: "127.0.0.1",
port: 9090,
});
assert.ok(created?.id);
// management route uses the snake_case param name; the usage branch must fire.
const result = await resolveProxyLookupResponse(
new URLSearchParams({ id: created.id, where_used: "1" }),
"where_used"
);
assert.ok(result instanceof Response);
assert.equal(result.status, 200);
// getProxyWhereUsed returns a usage payload (array/object), not the proxy row.
const body = await result.json();
assert.notEqual((body as Record<string, unknown>)?.name, "Usage Proxy");
});