fix: address code review findings for DNS management
- CRITICAL: Change DNS zones endpoint from GET to POST to avoid leaking API token in URL query parameters - HIGH: Add sync.RWMutex to protect dnsProvider field in Server, Deployer, and proxy Manager against concurrent read/write races - HIGH: Capture old DNS provider reference synchronously before launching background cleanup goroutine - HIGH: Use getDNS()/getDNSProviderLocked() accessors instead of direct field reads in all DNS operations
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import { e as escape_html, s as store_get, u as unsubscribe_stores, f as ensure_array_like, a as attr, i as derived, h as head } from "../../../../chunks/index.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
import { t } from "../../../../chunks/index2.js";
|
||||
import { v as validateProxy } from "../../../../chunks/api.js";
|
||||
import { F as FormField } from "../../../../chunks/FormField.js";
|
||||
import { I as IconCheck } from "../../../../chunks/IconCheck.js";
|
||||
import { I as IconX } from "../../../../chunks/IconX.js";
|
||||
import { I as IconLoader } from "../../../../chunks/IconLoader.js";
|
||||
import { I as IconGlobe } from "../../../../chunks/IconGlobe.js";
|
||||
function ValidationChecklist($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
var $$store_subs;
|
||||
const { result, loading = false } = $$props;
|
||||
const stepLabelKeys = {
|
||||
syntax: "proxies.validation.syntax",
|
||||
dns: "proxies.validation.dns",
|
||||
tcp: "proxies.validation.tcp",
|
||||
http: "proxies.validation.http"
|
||||
};
|
||||
function getStepLabel(name) {
|
||||
const key = stepLabelKeys[name];
|
||||
return key ? store_get($$store_subs ??= {}, "$t", t)(key) : name;
|
||||
}
|
||||
if (loading || result) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-card)] p-4"><h4 class="text-sm font-medium text-[var(--text-primary)] mb-3">${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.validation.title"))}</h4> `);
|
||||
if (loading && !result) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<div class="flex items-center gap-2 text-sm text-[var(--text-secondary)]">`);
|
||||
IconLoader($$renderer2, { size: 16 });
|
||||
$$renderer2.push(`<!----> <span>${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.validation.checking"))}</span></div>`);
|
||||
} else if (result) {
|
||||
$$renderer2.push("<!--[1-->");
|
||||
$$renderer2.push(`<ul class="space-y-2"><!--[-->`);
|
||||
const each_array = ensure_array_like(result.steps);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let step = each_array[$$index];
|
||||
$$renderer2.push(`<li><div class="flex items-center gap-2">`);
|
||||
if (step.passed) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950">`);
|
||||
IconCheck($$renderer2, { size: 14, class: "text-emerald-600 dark:text-emerald-400" });
|
||||
$$renderer2.push(`<!----></span> <span class="text-sm text-[var(--text-primary)]">${escape_html(getStepLabel(step.name))}</span> `);
|
||||
if (step.message) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<span class="text-xs text-[var(--text-tertiary)]">— ${escape_html(step.message)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
$$renderer2.push(`<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-950">`);
|
||||
IconX($$renderer2, { size: 14, class: "text-red-600 dark:text-red-400" });
|
||||
$$renderer2.push(`<!----></span> <span class="text-sm text-[var(--text-primary)]">${escape_html(getStepLabel(step.name))}</span> `);
|
||||
if (step.message) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<span class="text-xs text-[var(--text-tertiary)]">— ${escape_html(step.message)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> `);
|
||||
if (!step.passed && step.hint) {
|
||||
$$renderer2.push("<!--[0-->");
|
||||
$$renderer2.push(`<p class="ml-7 mt-1 text-xs text-amber-600 dark:text-amber-400">${escape_html(step.hint)}</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></li>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></ul>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
if ($$store_subs) unsubscribe_stores($$store_subs);
|
||||
});
|
||||
}
|
||||
function ProxyForm($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
var $$store_subs;
|
||||
const { proxy } = $$props;
|
||||
let destinationUrl = proxy?.destination_url ?? "";
|
||||
let port = proxy?.destination_port?.toString() ?? "";
|
||||
let domain = proxy?.domain ?? "";
|
||||
let validationResult = null;
|
||||
let validating = false;
|
||||
let validationTimer = null;
|
||||
const portNum = derived(() => parseInt(port, 10));
|
||||
const portValid = derived(() => !isNaN(portNum()) && portNum() >= 1 && portNum() <= 65535);
|
||||
const canSubmit = derived(() => destinationUrl.trim().length > 0 && port.trim().length > 0 && portValid() && domain.trim().length > 0 && true);
|
||||
const title = derived(
|
||||
() => store_get($$store_subs ??= {}, "$t", t)("proxies.form.title")
|
||||
);
|
||||
const submitLabel = derived(
|
||||
() => store_get($$store_subs ??= {}, "$t", t)("proxies.form.create")
|
||||
);
|
||||
function suggestDomain(dest) {
|
||||
if (!dest) return "";
|
||||
try {
|
||||
const withScheme = dest.includes("://") ? dest : `http://${dest}`;
|
||||
const url = new URL(withScheme);
|
||||
const host = url.hostname;
|
||||
const cleaned = host.replace(/^(www|api|app)\./, "").replace(/\.\w+$/, "").replace(/[^a-z0-9.-]/gi, "-").toLowerCase();
|
||||
return cleaned || "";
|
||||
} catch {
|
||||
return dest.replace(/[^a-z0-9.-]/gi, "-").toLowerCase();
|
||||
}
|
||||
}
|
||||
function scheduleValidation() {
|
||||
if (validationTimer !== null) {
|
||||
clearTimeout(validationTimer);
|
||||
}
|
||||
validationResult = null;
|
||||
if (!destinationUrl.trim() || !port.trim() || !portValid()) {
|
||||
return;
|
||||
}
|
||||
validationTimer = setTimeout(
|
||||
() => {
|
||||
runValidation();
|
||||
},
|
||||
300
|
||||
);
|
||||
}
|
||||
async function runValidation() {
|
||||
if (!destinationUrl.trim() || !portValid()) return;
|
||||
validating = true;
|
||||
try {
|
||||
validationResult = await validateProxy(destinationUrl.trim(), portNum());
|
||||
} catch {
|
||||
validationResult = null;
|
||||
} finally {
|
||||
validating = false;
|
||||
}
|
||||
}
|
||||
function handleDestinationInput() {
|
||||
{
|
||||
const suggested = suggestDomain(destinationUrl);
|
||||
if (!domain || domain === suggestDomain(destinationUrl.slice(0, -1))) {
|
||||
domain = suggested;
|
||||
}
|
||||
}
|
||||
scheduleValidation();
|
||||
}
|
||||
function handlePortInput() {
|
||||
scheduleValidation();
|
||||
}
|
||||
let $$settled = true;
|
||||
let $$inner_renderer;
|
||||
function $$render_inner($$renderer3) {
|
||||
$$renderer3.push(`<div class="space-y-6"><h3 class="text-lg font-semibold text-[var(--text-primary)]">${escape_html(title())}</h3> <form class="space-y-4">`);
|
||||
FormField($$renderer3, {
|
||||
label: store_get($$store_subs ??= {}, "$t", t)("proxies.form.destination"),
|
||||
name: "destination_url",
|
||||
placeholder: "192.168.1.100 or http://my-service",
|
||||
required: true,
|
||||
oninput: handleDestinationInput,
|
||||
get value() {
|
||||
return destinationUrl;
|
||||
},
|
||||
set value($$value) {
|
||||
destinationUrl = $$value;
|
||||
$$settled = false;
|
||||
}
|
||||
});
|
||||
$$renderer3.push(`<!----> `);
|
||||
FormField($$renderer3, {
|
||||
label: store_get($$store_subs ??= {}, "$t", t)("proxies.form.port"),
|
||||
name: "destination_port",
|
||||
type: "number",
|
||||
placeholder: "8080",
|
||||
required: true,
|
||||
error: port && !portValid() ? store_get($$store_subs ??= {}, "$t", t)("validation.invalidPort") : "",
|
||||
oninput: handlePortInput,
|
||||
get value() {
|
||||
return port;
|
||||
},
|
||||
set value($$value) {
|
||||
port = $$value;
|
||||
$$settled = false;
|
||||
}
|
||||
});
|
||||
$$renderer3.push(`<!----> `);
|
||||
FormField($$renderer3, {
|
||||
label: store_get($$store_subs ??= {}, "$t", t)("proxies.form.domain"),
|
||||
name: "domain",
|
||||
placeholder: "my-service.example.com",
|
||||
required: true,
|
||||
helpText: store_get($$store_subs ??= {}, "$t", t)("proxies.form.domainHelp"),
|
||||
get value() {
|
||||
return domain;
|
||||
},
|
||||
set value($$value) {
|
||||
domain = $$value;
|
||||
$$settled = false;
|
||||
}
|
||||
});
|
||||
$$renderer3.push(`<!----> <div class="space-y-2">`);
|
||||
ValidationChecklist($$renderer3, { result: validationResult, loading: validating });
|
||||
$$renderer3.push(`<!----> <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border-primary)] px-3 py-1.5 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"${attr("disabled", !destinationUrl.trim() || !portValid() || validating, true)}>`);
|
||||
if (validating) {
|
||||
$$renderer3.push("<!--[0-->");
|
||||
IconLoader($$renderer3, { size: 14 });
|
||||
$$renderer3.push(`<!----> ${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.form.validating"))}`);
|
||||
} else {
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
$$renderer3.push(`${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.form.validate"))}`);
|
||||
}
|
||||
$$renderer3.push(`<!--]--></button></div> `);
|
||||
if (validationResult && !validationResult.valid) {
|
||||
$$renderer3.push("<!--[0-->");
|
||||
$$renderer3.push(`<p class="text-xs text-amber-600 dark:text-amber-400">Validation reported issues but you can still create the proxy.</p>`);
|
||||
} else {
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer3.push(`<!--]--> `);
|
||||
{
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer3.push(`<!--]--> <div class="flex items-center justify-between pt-2"><div>`);
|
||||
{
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer3.push(`<!--]--></div> <div class="flex items-center gap-3"><button type="button" class="rounded-lg px-4 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] transition-colors">${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.form.cancel"))}</button> <button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-[var(--color-brand-700)] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-brand-600)] disabled:opacity-50 disabled:cursor-not-allowed"${attr("disabled", !canSubmit(), true)}>`);
|
||||
{
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer3.push(`<!--]--> ${escape_html(submitLabel())}</button></div></div></form></div> `);
|
||||
{
|
||||
$$renderer3.push("<!--[-1-->");
|
||||
}
|
||||
$$renderer3.push(`<!--]-->`);
|
||||
}
|
||||
do {
|
||||
$$settled = true;
|
||||
$$inner_renderer = $$renderer2.copy();
|
||||
$$render_inner($$inner_renderer);
|
||||
} while (!$$settled);
|
||||
$$renderer2.subsume($$inner_renderer);
|
||||
if ($$store_subs) unsubscribe_stores($$store_subs);
|
||||
});
|
||||
}
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
var $$store_subs;
|
||||
head("1eyu80y", $$renderer2, ($$renderer3) => {
|
||||
$$renderer3.title(($$renderer4) => {
|
||||
$$renderer4.push(`<title>${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.form.title"))} - ${escape_html(store_get($$store_subs ??= {}, "$t", t)("app.name"))}</title>`);
|
||||
});
|
||||
});
|
||||
$$renderer2.push(`<div class="mb-6"><a href="/proxies" class="inline-flex items-center gap-1 text-sm text-[var(--text-secondary)] hover:text-[var(--color-brand-600)] transition-colors"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"></path><path d="m12 19-7-7 7-7"></path></svg> ${escape_html(store_get($$store_subs ??= {}, "$t", t)("common.back"))}</a></div> <div class="mb-6 flex items-center gap-3"><div class="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-brand-50)] text-[var(--color-brand-600)]">`);
|
||||
IconGlobe($$renderer2, { size: 22 });
|
||||
$$renderer2.push(`<!----></div> <h1 class="text-xl font-bold text-[var(--text-primary)]">${escape_html(store_get($$store_subs ??= {}, "$t", t)("proxies.form.title"))}</h1></div> <div class="mx-auto max-w-2xl rounded-2xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-6 shadow-[var(--shadow-sm)]">`);
|
||||
ProxyForm($$renderer2, {});
|
||||
$$renderer2.push(`<!----></div>`);
|
||||
if ($$store_subs) unsubscribe_stores($$store_subs);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
Reference in New Issue
Block a user