mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(proxy): address PR review findings for auth, credentials, and health stats
This commit is contained in:
@@ -155,18 +155,25 @@ export default function ProxyRegistryManager() {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
const payload = {
|
||||
const normalizedUsername = form.username.trim();
|
||||
const normalizedPassword = form.password.trim();
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
...(editingId ? { id: editingId } : {}),
|
||||
name: form.name.trim(),
|
||||
type: form.type,
|
||||
host: form.host.trim(),
|
||||
port: Number(form.port || 8080),
|
||||
username: form.username.trim(),
|
||||
password: form.password.trim(),
|
||||
region: form.region.trim() || null,
|
||||
notes: form.notes.trim() || null,
|
||||
status: form.status,
|
||||
};
|
||||
if (!editingId || normalizedUsername.length > 0) {
|
||||
payload.username = normalizedUsername;
|
||||
}
|
||||
if (!editingId || normalizedPassword.length > 0) {
|
||||
payload.password = normalizedPassword;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings/proxies", {
|
||||
@@ -474,6 +481,7 @@ export default function ProxyRegistryManager() {
|
||||
<input
|
||||
className="w-full px-3 py-2 rounded bg-bg-subtle border border-border"
|
||||
value={form.username}
|
||||
placeholder={editingId ? "Leave blank to keep current username" : ""}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
@@ -483,6 +491,7 @@ export default function ProxyRegistryManager() {
|
||||
type="password"
|
||||
className="w-full px-3 py-2 rounded bg-bg-subtle border border-border"
|
||||
value={form.password}
|
||||
placeholder={editingId ? "Leave blank to keep current password" : ""}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { migrateLegacyProxyConfigToRegistry } from "@/lib/localDb";
|
||||
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { z } from "zod";
|
||||
|
||||
const migrateLegacyProxySchema = z.object({
|
||||
force: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let force = false;
|
||||
let rawBody: unknown;
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
force = body?.force === true;
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({
|
||||
status: 400,
|
||||
@@ -16,6 +21,17 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const validation = validateBody(migrateLegacyProxySchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return createErrorResponse({
|
||||
status: 400,
|
||||
message: validation.error.message,
|
||||
details: validation.error.details,
|
||||
type: "invalid_request",
|
||||
});
|
||||
}
|
||||
|
||||
const force = validation.data.force === true;
|
||||
const result = await migrateLegacyProxyConfigToRegistry({ force });
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { assignProxyToScope, getProxyAssignments, resolveProxyForConnection } fr
|
||||
import { proxyAssignmentSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
|
||||
function toPagination(searchParams: URLSearchParams) {
|
||||
@@ -11,6 +12,9 @@ function toPagination(searchParams: URLSearchParams) {
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const proxyId = searchParams.get("proxy_id");
|
||||
@@ -42,6 +46,9 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -2,9 +2,13 @@ import { bulkAssignProxyToScope } from "@/lib/localDb";
|
||||
import { bulkProxyAssignmentSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { getProxyHealthStats } from "@/lib/localDb";
|
||||
import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const hours = Number(searchParams.get("hours") || 24);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
function toPagination(searchParams: URLSearchParams) {
|
||||
const limit = Math.max(1, Math.min(200, Number(searchParams.get("limit") || 50)));
|
||||
@@ -17,6 +18,9 @@ function toPagination(searchParams: URLSearchParams) {
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -48,6 +52,9 @@ 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();
|
||||
@@ -78,6 +85,9 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -113,6 +123,9 @@ export async function PATCH(request: 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");
|
||||
|
||||
18
src/lib/api/requireManagementAuth.ts
Normal file
18
src/lib/api/requireManagementAuth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isAuthenticated, isAuthRequired } from "@/shared/utils/apiAuth";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
|
||||
export async function requireManagementAuth(request: Request): Promise<Response | null> {
|
||||
if (!(await isAuthRequired())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await isAuthenticated(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createErrorResponse({
|
||||
status: 401,
|
||||
message: "Authentication required",
|
||||
type: "invalid_request",
|
||||
});
|
||||
}
|
||||
@@ -197,9 +197,23 @@ export async function updateProxy(id: string, payload: Partial<ProxyPayload>) {
|
||||
const existing = await getProxyById(id, { includeSecrets: true });
|
||||
if (!existing) return null;
|
||||
|
||||
const incomingUsername =
|
||||
typeof payload.username === "string" ? payload.username.trim() : undefined;
|
||||
const incomingPassword =
|
||||
typeof payload.password === "string" ? payload.password.trim() : undefined;
|
||||
|
||||
const merged = {
|
||||
...existing,
|
||||
...payload,
|
||||
// Preserve stored credentials unless caller explicitly sends non-empty replacements.
|
||||
username:
|
||||
incomingUsername === undefined || incomingUsername.length === 0
|
||||
? existing.username
|
||||
: incomingUsername,
|
||||
password:
|
||||
incomingPassword === undefined || incomingPassword.length === 0
|
||||
? existing.password
|
||||
: incomingPassword,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -508,6 +522,7 @@ export async function getProxyHealthStats(options?: { hours?: number }) {
|
||||
LEFT JOIN proxy_logs l
|
||||
ON l.proxy_host = p.host
|
||||
AND l.proxy_type = p.type
|
||||
AND l.proxy_port = p.port
|
||||
AND l.timestamp >= ?
|
||||
GROUP BY p.id, p.name, p.type, p.host, p.port
|
||||
ORDER BY p.name ASC`
|
||||
|
||||
@@ -207,6 +207,21 @@ export default function ProxyConfigModal({
|
||||
await fetch(`/api/settings/proxy?${clearParams.toString()}`, { method: "DELETE" });
|
||||
}
|
||||
} else {
|
||||
const clearAssignmentRes = await fetch("/api/settings/proxies/assignments", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
scope,
|
||||
scopeId: level === "global" ? null : levelId,
|
||||
proxyId: null,
|
||||
}),
|
||||
});
|
||||
const clearAssignmentPayload = await clearAssignmentRes.json().catch(() => ({}));
|
||||
if (!clearAssignmentRes.ok) {
|
||||
setFormError(clearAssignmentPayload?.error?.message || "Failed to clear saved proxy");
|
||||
return;
|
||||
}
|
||||
|
||||
const proxy = {
|
||||
type: proxyType,
|
||||
host: host.trim(),
|
||||
@@ -226,6 +241,9 @@ export default function ProxyConfigModal({
|
||||
return;
|
||||
}
|
||||
setHasOwnProxy(true);
|
||||
if (mode === "custom") {
|
||||
setSelectedProxyId("");
|
||||
}
|
||||
onSaved?.();
|
||||
} catch (error) {
|
||||
console.error("Error saving proxy:", error);
|
||||
|
||||
@@ -211,6 +211,9 @@ test.describe("Proxy Registry smoke flow", () => {
|
||||
.toBe(1);
|
||||
|
||||
await row.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(page.locator("table")).not.toContainText("Registry Smoke Proxy");
|
||||
await expect
|
||||
.poll(() => state.proxies.some((proxy) => proxy.name === "Registry Smoke Proxy"))
|
||||
.toBe(false);
|
||||
await expect(page.locator("tr", { hasText: "Registry Smoke Proxy" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user