293 lines
23 KiB
JavaScript
293 lines
23 KiB
JavaScript
"use strict";
|
|
// Shared, safe controls: HTML is edited as text, never injected into the admin DOM.
|
|
(() => {
|
|
const label = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
|
const option = (value, key) => { const node = label("option", key); node.value = value; return node; };
|
|
const codes = [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504];
|
|
const STATUS_NAMES = { 200: "OK", 204: "No Content", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 408: "Request Timeout", 410: "Gone", 413: "Payload Too Large", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout" };
|
|
const statusLabel = code => `${code} ${STATUS_NAMES[code] || ""}`.trim();
|
|
function pageEditor(parent, key, value = null) {
|
|
const row = el("div", undefined, "page-editor"), heading = label("strong", key, "page-label"); row.append(heading);
|
|
const mode = el("select"), status = el("select"), html = el("textarea"), file = el("input");
|
|
mode.setAttribute("aria-label", t(key)); mode.className = "page-mode";
|
|
mode.append(option("", "pageInherit"), option("welcome", "pageWelcome"), option("builtin", "pageBuiltin"), option("empty", "pageEmpty"), option("html", "pageHtml"));
|
|
mode.value = value?.kind || "";
|
|
status.className = "page-status"; status.setAttribute("aria-label", t("pageStatus"));
|
|
status.append(option("", "pageOriginalStatus"));
|
|
for (const code of [200, 204, 400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504]) { const o = el("option", statusLabel(code)); o.value = code; status.append(o); }
|
|
status.value = value?.status ?? ""; html.value = value?.html || ""; html.rows = 5; html.maxLength = 32768; html.spellcheck = false; html.className = "page-html"; html.setAttribute("aria-label", t("pageHtmlSource"));
|
|
file.type = "file"; file.accept = ".html,.htm,text/html,text/plain"; file.setAttribute("aria-label", t("pageUploadHtml"));
|
|
const source = el("div", undefined, "page-source"); source.append(label("span", "pageUploadHtml", "fine"), file, html);
|
|
function sync() { status.disabled = !mode.value; source.hidden = mode.value !== "html"; if (status.value === "204" && mode.value !== "empty") status.value = ""; }
|
|
for (const event of ["change", "input"]) { mode.addEventListener(event, sync); status.addEventListener(event, sync); }
|
|
file.addEventListener("change", async () => { try { const chosen = file.files[0]; if (!chosen) return; if (chosen.size > 32768) throw new Error(t("pageTooLarge")); html.value = await chosen.text(); html.dispatchEvent(new Event("input", { bubbles: true })); } catch (error) { toast(error.message, true); } finally { file.value = ""; } });
|
|
row.append(mode, status, source); parent.append(row); sync();
|
|
return { row, read() { if (!mode.value) return null; const markup = mode.value === "html" ? html.value : ""; if (new TextEncoder().encode(markup).length > 32768) throw new Error(t("pageTooLarge")); return { kind: mode.value, status: status.value ? Number(status.value) : null, html: markup }; } };
|
|
}
|
|
function errorsEditor(parent, values = {}) {
|
|
const details = el("details", undefined, "pages-errors"); details.append(label("summary", "customErrorPages")); parent.append(details);
|
|
const rows = new Map(); for (const code of codes) { const group = el("div"); details.append(group); const editor = pageEditor(group, "customErrorPages", values[code]); editor.row.querySelector("strong").removeAttribute("data-i18n"); editor.row.querySelector("strong").textContent = statusLabel(code); rows.set(code, editor); }
|
|
return { read() { return Object.fromEntries([...rows].map(([code, editor]) => [code, editor.read()]).filter(([, page]) => page)); } };
|
|
}
|
|
function vhostEditor(parent, values = {}) {
|
|
const response = pageEditor(parent, "pageStaticHost", values.response), fallback = pageEditor(parent, "pageMissingPath", values.fallback), errors = errorsEditor(parent, values.errors);
|
|
parent.append(label("p", "pagesSafetyHint", "fine")); return { read: () => ({ response: response.read(), fallback: fallback.read(), errors: errors.read() }) };
|
|
}
|
|
let global = null;
|
|
function render() { if (!envelope || settingsDirty.has("pages-form")) return; const values = envelope.config.pages || {}, parent = $("global-pages-fields"); parent.replaceChildren(); const ip = pageEditor(parent, "pageDirectIp", values.direct_ip), unknown = pageEditor(parent, "pageUnknownHost", values.unknown_host), errors = errorsEditor(parent, values.errors); global = { read: () => ({ direct_ip: ip.read(), unknown_host: unknown.read(), errors: errors.read() }) }; }
|
|
window.rustProxyPages = { render, vhostEditor };
|
|
bindSettingsForm("pages-form", next => { next.pages = global.read(); }, "pages");
|
|
$("settings-security-open").addEventListener("click", () => setPanel("security"));
|
|
$("settings-vhost-open").addEventListener("click", () => setPanel("vhosts"));
|
|
if (envelope) render();
|
|
})();
|
|
|
|
(() => {
|
|
const dialog = $("logs-dialog"); let paused = null, expanded = null, docked = false, pinning = false, preferencesLoaded = false, preferencesLoading = null, preferenceSaveRunning = false, preferenceSaveQueued = false;
|
|
const filters = ["log-vhost", "log-method", "log-status", "log-outcome", "log-filter"];
|
|
const columns = [
|
|
{ id: "time", label: "time", value: e => formatTime(e.time_ms) },
|
|
{ id: "request", label: "request", value: e => `${e.method} ${e.path}` },
|
|
{ id: "host", label: "logHost", value: e => e.host },
|
|
{ id: "source_ip", label: "sourceIp", value: e => e.client_ip || "-" },
|
|
{ id: "vhost", label: "vhVhost", value: e => e.vhost || "-" },
|
|
{ id: "backend", label: "backend", value: e => e.backend || "-" },
|
|
{ id: "status", label: "status", value: e => e.status },
|
|
{ id: "duration", label: "duration", value: e => `${e.duration_ms} ms` },
|
|
{ id: "cache", label: "cache", value: e => e.cache || "-" },
|
|
{ id: "outcome", label: "outcome", value: e => t(e.outcome) },
|
|
{ id: "transport", label: "logTransport", value: e => e.transport ? `${e.transport}${e.http_version ? ` / HTTP ${e.http_version}` : ""}` : "-" },
|
|
{ id: "websocket", label: "logWebSocket", value: e => [e.websocket_bootstrap, e.websocket_phase, e.websocket_error, e.h3_reset_code].filter(Boolean).join(" / ") || "-" },
|
|
{ id: "upstream_error", label: "logUpstreamError", value: e => e.upstream_error || "-" },
|
|
];
|
|
const defaultColumns = () => new Set(columns.map(column => column.id));
|
|
let visibleColumns = defaultColumns();
|
|
function source() { return paused || expanded || lastLogs || { entries: [] }; }
|
|
function mergeExpanded(data) {
|
|
if (!expanded || paused || !data?.entries) return;
|
|
const seen = new Set();
|
|
const entries = [];
|
|
for (const entry of [...data.entries, ...(expanded.entries || [])]) {
|
|
const key = entry.id ?? `${entry.time_ms}:${entry.method}:${entry.host}:${entry.path}:${entry.status}`;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key); entries.push(entry);
|
|
if (entries.length >= 4096) break;
|
|
}
|
|
expanded = { ...expanded, ...data, entries, capacity: 4096, window: 4096 };
|
|
}
|
|
function syncExpandedButton() {
|
|
const button = $("logs-load-history");
|
|
button.textContent = t(expanded ? "logExpandedBuffer" : "logExpandBuffer");
|
|
button.dataset.i18n = expanded ? "logExpandedBuffer" : "logExpandBuffer";
|
|
button.disabled = Boolean(expanded);
|
|
}
|
|
function filtered() {
|
|
const query = $("log-filter").value.trim().toLowerCase(), outcome = $("log-outcome").value;
|
|
return source().entries.filter(e => (!$("log-vhost").value || e.vhost === $("log-vhost").value)
|
|
&& (!$("log-method").value || e.method === $("log-method").value)
|
|
&& (!$("log-status").value || String(e.status).startsWith($("log-status").value))
|
|
&& (outcome === "all" || (outcome === "errors" ? e.status >= 400 : e.cache === outcome))
|
|
&& `${e.method} ${e.path} ${e.host} ${e.client_ip || ""} ${e.vhost || ""} ${e.backend || ""} ${e.transport || ""} ${e.http_version || ""} ${e.websocket_bootstrap || ""} ${e.websocket_phase || ""} ${e.websocket_error || ""} ${e.h3_reset_code || ""} ${e.upstream_error || ""}`.toLowerCase().includes(query));
|
|
}
|
|
function syncHeaders() {
|
|
dialog.querySelectorAll("th[data-log-column]").forEach(header => { header.hidden = !visibleColumns.has(header.dataset.logColumn); });
|
|
}
|
|
function syncColumnPicker() {
|
|
const picker = $("log-column-options");
|
|
picker.replaceChildren(...columns.map(column => {
|
|
const label = el("label", undefined, "check log-column-option");
|
|
const input = document.createElement("input"); input.type = "checkbox"; input.checked = visibleColumns.has(column.id); input.dataset.column = column.id;
|
|
const text = el("span", t(column.label)); text.dataset.i18n = column.label;
|
|
input.addEventListener("change", () => {
|
|
if (!input.checked && visibleColumns.size === 1) { input.checked = true; toast(t("logColumnsRequired"), true); return; }
|
|
if (input.checked) visibleColumns.add(column.id); else visibleColumns.delete(column.id);
|
|
syncHeaders(); render(); void savePreferences();
|
|
});
|
|
label.append(input, text); return label;
|
|
}));
|
|
}
|
|
function applyPreferences(preferences) {
|
|
if (!preferences) return;
|
|
const selected = Array.isArray(preferences.live_log_columns) ? preferences.live_log_columns.filter(id => columns.some(column => column.id === id)) : [];
|
|
visibleColumns = selected.length ? new Set(selected) : defaultColumns(); preferencesLoaded = true;
|
|
syncColumnPicker(); syncHeaders(); render();
|
|
}
|
|
async function loadPreferences(force = false) {
|
|
if (!authenticated || (preferencesLoaded && !force)) return;
|
|
if (preferencesLoading) return preferencesLoading;
|
|
preferencesLoading = (async () => {
|
|
try { applyPreferences(await window.rustProxyPreferences.load(force)); }
|
|
catch (error) { if (error.status !== 401) toast(error.message, true); }
|
|
finally { preferencesLoading = null; }
|
|
})();
|
|
return preferencesLoading;
|
|
}
|
|
async function savePreferences() {
|
|
if (!authenticated) return;
|
|
preferenceSaveQueued = true;
|
|
if (preferenceSaveRunning) return;
|
|
preferenceSaveRunning = true;
|
|
try {
|
|
while (preferenceSaveQueued && authenticated) {
|
|
preferenceSaveQueued = false;
|
|
const selected = columns.filter(column => visibleColumns.has(column.id)).map(column => column.id);
|
|
const preferences = await window.rustProxyPreferences.update({ live_log_columns: selected });
|
|
if (!authenticated) return;
|
|
if (!preferenceSaveQueued) applyPreferences(preferences);
|
|
}
|
|
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
|
finally { preferenceSaveRunning = false; }
|
|
}
|
|
function render(data = null) {
|
|
if (data) mergeExpanded(data);
|
|
if (!dialog.open) return;
|
|
const selector = $("log-vhost"), selected = selector.value, ids = new Set([...(envelope?.config.vhosts || []).map(v => v.id), ...source().entries.map(e => e.vhost).filter(Boolean)]);
|
|
const all = el("option", t("logAllVhosts")); all.value = ""; selector.replaceChildren(all, ...[...ids].sort().map(id => { const o = el("option", id); o.value = id; return o; })); if (ids.has(selected)) selector.value = selected;
|
|
const entries = filtered(), rows = entries.map(e => {
|
|
const row = el("tr"); const hints = [];
|
|
if (e.cache_reason) hints.push(window.rustProxyCachePanel?.reason(e.cache_reason) || e.cache_reason);
|
|
if (e.upstream_error) hints.push(`upstream: ${e.upstream_error}`);
|
|
row.title = hints.join(" · ");
|
|
for (const column of columns) {
|
|
if (!visibleColumns.has(column.id)) continue;
|
|
const value = column.value(e), cell = el("td", String(value)); cell.title = String(value);
|
|
if (column.id === "status") cell.className = e.status < 400 ? "status-good" : "status-error";
|
|
if (column.id === "cache" && e.cache === "HIT") cell.className = "status-good";
|
|
row.append(cell);
|
|
}
|
|
return row;
|
|
});
|
|
syncHeaders(); $("logs").replaceChildren(...rows); $("empty-logs").hidden = rows.length > 0; $("log-count").textContent = `${entries.length} / ${source().entries.length} | ${t(paused ? "logPaused" : "live")}`;
|
|
$("logs-pause").textContent = t(paused ? "logResume" : "logPause"); $("logs-pin").textContent = t(docked ? "logUndock" : "logPin"); syncExpandedButton();
|
|
}
|
|
function open(vhost) {
|
|
if (!authenticated) return;
|
|
if (!preferencesLoaded) void loadPreferences();
|
|
if (vhost) { const opt = el("option", vhost); opt.value = vhost; $("log-vhost").append(opt); $("log-vhost").value = vhost; }
|
|
if (!dialog.open) { if (docked) dialog.show(); else dialog.showModal(); }
|
|
render();
|
|
}
|
|
syncColumnPicker(); syncHeaders();
|
|
$("logs-open").addEventListener("click", () => { $("logs-menu").open = false; open(); }); $("logs-close").addEventListener("click", () => dialog.close());
|
|
$("logs-load-history").addEventListener("click", async () => {
|
|
const button = $("logs-load-history"); button.disabled = true;
|
|
try {
|
|
const snapshot = await api("/api/logs?limit=4096");
|
|
expanded = { ...snapshot, capacity: 4096, window: 4096 }; render();
|
|
toast(t("logBufferExpanded").replace("{count}", String(snapshot.entries?.length || 0)));
|
|
} catch (error) { toast(error.message, true); }
|
|
finally { button.disabled = Boolean(expanded); }
|
|
});
|
|
$("logs-pause").addEventListener("click", () => { paused = paused ? null : structuredClone(source()); render(); });
|
|
$("logs-pin").addEventListener("click", () => { pinning = true; dialog.close(); docked = !docked; dialog.classList.toggle("docked", docked); if (docked) dialog.show(); else dialog.showModal(); pinning = false; render(); });
|
|
dialog.addEventListener("close", () => { if (pinning) return; expanded = null; paused = null; syncExpandedButton(); });
|
|
for (const id of filters) $(id).addEventListener(id === "log-filter" ? "input" : "change", render);
|
|
$("export-logs").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify({ entries: filtered(), capacity: source().capacity, filtered: true }, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = "RustProxy-access-log.json"; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
|
window.rustProxyLogs = { render, open, loadPreferences, applyPreferences, reset() { dialog.close(); paused = null; expanded = null; preferencesLoaded = false; preferencesLoading = null; preferenceSaveQueued = false; visibleColumns = defaultColumns(); syncColumnPicker(); syncHeaders(); for (const id of filters) $(id).value = id === "log-outcome" ? "all" : ""; syncExpandedButton(); } };
|
|
})();
|
|
|
|
// Reusable ACL profiles. They are intentionally composed with, not substituted
|
|
// for, the per-vhost policy so attaching a profile cannot weaken a vhost rule.
|
|
(() => {
|
|
let editing = null, dirty = false;
|
|
const form = $("acl-profile-form");
|
|
const split = value => value.split(/[\s,]+/).map(v => v.trim()).filter(Boolean);
|
|
const ipEnabled = $("acl-profile-ip-enabled"), aclDefault = $("acl-profile-default"), rules = $("acl-profile-rules"), ruleRows = $("acl-profile-rule-rows");
|
|
const addButton = $("acl-profile-rule-add"), importButton = $("acl-profile-import-rules");
|
|
const i18n = (tag, key) => { const node = el(tag, t(key)); node.dataset.i18n = key; return node; };
|
|
function addRule(rule = { action: "allow", network: "" }, mark = true) {
|
|
if (ruleRows.children.length >= 128) { toast(t("vhRowLimit"), true); return; }
|
|
const row = el("div", undefined, "acl-rule acl-profile-rule");
|
|
const actionLabel = el("label"); actionLabel.append(i18n("span", "aclAction"));
|
|
const actionSelect = el("select"); actionSelect.className = "acl-profile-rule-action";
|
|
for (const value of ["allow", "deny"]) { const option = el("option", t(value === "allow" ? "aclAllow" : "vhDeny")); option.value = value; actionSelect.append(option); }
|
|
actionSelect.value = rule.action || "allow"; actionLabel.append(actionSelect);
|
|
const networkLabel = el("label"); networkLabel.append(i18n("span", "aclNetwork"));
|
|
const network = el("input"); network.className = "acl-profile-rule-network"; network.type = "text"; network.required = true; network.maxLength = 64; network.autocomplete = "off"; network.spellcheck = false; network.placeholder = "10.0.0.0/8"; network.value = rule.network || ""; networkLabel.append(network);
|
|
const remove = action(t("remove"), () => { row.remove(); syncRulesText(); dirty = true; syncIp(); }); remove.classList.add("acl-profile-rule-remove");
|
|
row.append(actionLabel, networkLabel, remove); ruleRows.append(row);
|
|
if (mark) { syncRulesText(); dirty = true; }
|
|
syncIp();
|
|
}
|
|
function collectRuleRows() {
|
|
return [...ruleRows.children].map(row => ({
|
|
action: row.querySelector(".acl-profile-rule-action").value,
|
|
network: row.querySelector(".acl-profile-rule-network").value.trim(),
|
|
}));
|
|
}
|
|
function syncRulesText() { rules.value = collectRuleRows().map(rule => `${rule.action} ${rule.network}`.trimEnd()).join("\n"); }
|
|
function renderRuleRows(values) { ruleRows.replaceChildren(); for (const rule of values || []) addRule(rule, false); syncRulesText(); syncIp(); }
|
|
function importRules() {
|
|
try { renderRuleRows(parseRules(rules.value, rules)); dirty = true; toast(t("aclProfileRulesImported")); }
|
|
catch (error) { toast(error.message, true); }
|
|
}
|
|
function syncIp() {
|
|
const disabled = !ipEnabled.checked;
|
|
aclDefault.disabled = disabled; rules.disabled = disabled; addButton.disabled = disabled; importButton.disabled = disabled;
|
|
for (const node of ruleRows.querySelectorAll("input,select,button")) node.disabled = disabled;
|
|
}
|
|
function clear() {
|
|
editing = null; dirty = false; form.reset(); ipEnabled.checked = false; aclDefault.value = "deny"; $("acl-profile-country-mode").value = "off"; $("acl-profile-id").readOnly = false; $("acl-profile-editing").textContent = t("aclProfileNew"); renderRuleRows([]); rules.value = ""; syncIp();
|
|
}
|
|
function edit(profile) {
|
|
editing = profile.id; dirty = false; $("acl-profile-id").value = profile.id; $("acl-profile-id").readOnly = true; $("acl-profile-name").value = profile.name || profile.id;
|
|
ipEnabled.checked = !!profile.acl; aclDefault.value = profile.acl?.default || "deny"; renderRuleRows(profile.acl?.rules || []);
|
|
$("acl-profile-country-mode").value = profile.country_access?.mode || "off"; $("acl-profile-countries").value = (profile.country_access?.countries || []).join("\n");
|
|
$("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncIp();
|
|
}
|
|
function render() {
|
|
if (!envelope) return;
|
|
const profiles = envelope.config.acl_profiles || [], list = $("acl-profile-list");
|
|
const cards = profiles.map(profile => {
|
|
const box = el("div", undefined, "entity");
|
|
const top = el("div", undefined, "entity-top"); top.append(el("span", profile.name || profile.id, "entity-title"), el("small", profile.id)); box.append(top);
|
|
const pieces = [];
|
|
if (profile.acl) pieces.push(`${t("aclDefault")}: ${t(profile.acl.default || "deny")} · ${(profile.acl.rules || []).length} ${t("aclProfileIpRulesShort")}`);
|
|
else pieces.push(t("aclProfileIpOff"));
|
|
const countries = profile.country_access?.countries || [];
|
|
pieces.push(profile.country_access?.mode && profile.country_access.mode !== "off" ? `${t("vhCountryMode")}: ${t(profile.country_access.mode === "block" ? "vhCountryBlock" : "vhCountryAllow")} · ${countries.join(", ") || "-"}` : t("aclProfileCountriesOff"));
|
|
const users = (envelope.config.vhosts || []).filter(v => v.acl_profile_id === profile.id).map(v => v.id);
|
|
for (const agent of envelope.config.docker_agents?.agents || []) {
|
|
if (agent.acl_profile_id === profile.id) users.push(`docker-agent:${agent.id}`);
|
|
for (const assignment of agent.assignments || []) if (assignment.acl_profile_id === profile.id) users.push(`docker:${agent.id}/${assignment.hostname || assignment.service_key}`);
|
|
}
|
|
if (users.length) pieces.push(`${t("aclProfileAttached")}: ${users.join(", ")}`);
|
|
box.append(el("p", pieces.join("\n"), "entity-details"));
|
|
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => edit(profile)), action(t("remove"), async () => {
|
|
if (users.length) { toast(`${t("aclProfileInUse")}: ${users.join(", ")}`, true); return; }
|
|
if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.acl_profiles = (next.acl_profiles || []).filter(item => item.id !== profile.id); if (await saveSection("acl-profiles", next.acl_profiles || [])) clear();
|
|
})); box.append(actions); return box;
|
|
});
|
|
list.replaceChildren(...(cards.length ? cards : [el("p", t("aclProfilesEmpty"), "empty")]));
|
|
if (!dirty && editing) { const current = profiles.find(p => p.id === editing); if (current) edit(current); else clear(); }
|
|
}
|
|
form.addEventListener("input", () => { dirty = true; }); form.addEventListener("change", () => { dirty = true; });
|
|
ruleRows.addEventListener("input", syncRulesText); ruleRows.addEventListener("change", syncRulesText);
|
|
ipEnabled.addEventListener("change", () => { if (ipEnabled.checked && !ruleRows.children.length) addRule(); syncIp(); });
|
|
addButton.addEventListener("click", () => addRule()); importButton.addEventListener("click", importRules);
|
|
form.addEventListener("submit", async event => {
|
|
event.preventDefault(); if (!envelope || busy) return;
|
|
try {
|
|
await withFormSubmitState(form, event.submitter, async () => {
|
|
const id = $("acl-profile-id").value.trim(), name = $("acl-profile-name").value.trim();
|
|
const countries = [...new Set(split($("acl-profile-countries").value).map(v => v.toUpperCase()))];
|
|
if (countries.some(v => !/^[A-Z]{2}$/.test(v))) throw fieldError($("acl-profile-countries"), t("aclProfileCountryInvalid"));
|
|
// If the bulk textarea was edited directly, apply it before saving instead of
|
|
// silently ignoring pasted content. The parser reports the exact bad line.
|
|
const visualText = collectRuleRows().map(rule => `${rule.action} ${rule.network}`.trimEnd()).join("\n");
|
|
if (ipEnabled.checked && normalizeAclRuleText(rules.value).trim() !== normalizeAclRuleText(visualText).trim()) renderRuleRows(parseRules(rules.value, rules));
|
|
const aclRules = collectRuleRows();
|
|
if (ipEnabled.checked && aclRules.some(rule => !rule.network)) { const input = [...ruleRows.querySelectorAll(".acl-profile-rule-network")].find(node => !node.value.trim()); throw fieldError(input || rules, t("ruleSyntax")); }
|
|
const profile = { id, name, acl: ipEnabled.checked ? { default: aclDefault.value, rules: aclRules } : null, country_access: { mode: $("acl-profile-country-mode").value, countries } };
|
|
const next = structuredClone(envelope.config); next.acl_profiles ||= [];
|
|
const index = next.acl_profiles.findIndex(item => item.id === editing);
|
|
if (index < 0) { if (next.acl_profiles.some(item => item.id === id)) throw fieldError($("acl-profile-id"), t("aclProfileDuplicate")); next.acl_profiles.push(profile); } else next.acl_profiles[index] = profile;
|
|
if (await saveSection("acl-profiles", next.acl_profiles || [])) { editing = id; dirty = false; render(); }
|
|
});
|
|
} catch (error) { toast(error.message, true); }
|
|
});
|
|
$("acl-profile-new").addEventListener("click", clear); $("acl-profile-reset").addEventListener("click", clear);
|
|
window.rustProxyAclProfiles = { render, reset: clear };
|
|
clear(); if (envelope) render();
|
|
})();
|