mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
142
src/lib/api/proxyRegistryRouteHandlers.ts
Normal file
142
src/lib/api/proxyRegistryRouteHandlers.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
93
tests/unit/proxy-registry-route-handlers.test.ts
Normal file
93
tests/unit/proxy-registry-route-handlers.test.ts
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user