feat(nacos): add r-nacos compatibility
This commit is contained in:
parent
e0ad5dcf86
commit
3c04adbdd8
|
|
@ -1879,6 +1879,7 @@ dependencies = [
|
|||
name = "dbx-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"aes-gcm 0.10.3",
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
|
@ -1886,6 +1887,7 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"calamine",
|
||||
"cbc 0.1.2",
|
||||
"chrono",
|
||||
"csv",
|
||||
"deadpool-postgres",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Switch } from "@/components/ui/switch";
|
|||
import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType, HttpTunnelConfig, IdentifierCase, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
|
||||
import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
|
||||
|
|
@ -53,6 +53,7 @@ import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
|
|||
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
|
||||
import { driverInstallProgressChannel, driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
||||
import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
|
|
@ -114,9 +115,6 @@ const DREMIO_ARROW_FLIGHT_SQL_JDBC_URL = "jdbc:arrow-flight-sql://127.0.0.1:3201
|
|||
const DREMIO_ARROW_FLIGHT_SQL_JDBC_DRIVER_CLASS = "org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver";
|
||||
const DREMIO_LEGACY_JDBC_URL = "jdbc:dremio:direct=127.0.0.1:31010";
|
||||
const DREMIO_LEGACY_JDBC_DRIVER_CLASS = "com.dremio.jdbc.Driver";
|
||||
const NACOS_DEFAULT_CONSOLE_URL = "http://127.0.0.1:8085";
|
||||
const NACOS_LEGACY_SERVER_PORT = "8848";
|
||||
const NACOS_DOCKER_CONSOLE_PORT = "8085";
|
||||
const DEFAULT_SSH_USER = "root";
|
||||
|
||||
type LegacyTransportFields = {
|
||||
|
|
@ -586,14 +584,77 @@ const mqKafkaSaslMechanismOptions = [
|
|||
{ value: "SCRAM-SHA-256", label: "SCRAM-SHA-256" },
|
||||
{ value: "SCRAM-SHA-512", label: "SCRAM-SHA-512" },
|
||||
];
|
||||
const nacosServerAddr = ref(NACOS_DEFAULT_CONSOLE_URL);
|
||||
const nacosImplementation = ref<NacosImplementation>("nacos");
|
||||
const nacosVersionMode = ref<NacosVersionMode>("auto");
|
||||
const nacosServerAddr = ref("");
|
||||
const nacosNamespace = ref("");
|
||||
const nacosContextPath = ref("");
|
||||
const nacosContextPathCustomized = ref(false);
|
||||
const nacosRNacosConsoleAddr = ref("");
|
||||
const nacosHistoryEnabled = ref(false);
|
||||
const nacosConsoleAuthKind = ref<NacosRNacosConsoleAuth["kind"]>("inherit");
|
||||
const nacosConsoleUsername = ref("");
|
||||
const nacosConsolePassword = ref("");
|
||||
const nacosAuthKind = ref<NacosAuthKind>("none");
|
||||
const nacosUsername = ref("nacos");
|
||||
const nacosPassword = ref("");
|
||||
const nacosTlsSkipVerify = ref(false);
|
||||
const nacosPageSize = ref(20);
|
||||
const nacosPrimaryAddressLabel = computed(() => {
|
||||
if (nacosImplementation.value === "rnacos") return t("connection.nacosPrimaryAddressRNacos");
|
||||
if (nacosVersionMode.value === "v2") return t("connection.nacosPrimaryAddressV2");
|
||||
if (nacosVersionMode.value === "v3") return t("connection.nacosPrimaryAddressV3");
|
||||
return t("connection.nacosPrimaryAddressAuto");
|
||||
});
|
||||
const nacosPrimaryAddressPlaceholder = computed(() => {
|
||||
if (nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2") return "http://127.0.0.1:8848/nacos";
|
||||
return "http://127.0.0.1:8080";
|
||||
});
|
||||
const nacosNormalizedPreview = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) return "";
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return `${normalized.serverAddr}${normalized.contextPath || ""}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const nacosEffectiveContextPath = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2" ? "/nacos" : "/";
|
||||
}
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return normalized.contextPath || "/";
|
||||
} catch {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/";
|
||||
}
|
||||
});
|
||||
const nacosContextPathInput = computed({
|
||||
get: () => (nacosContextPathCustomized.value ? nacosContextPath.value : nacosEffectiveContextPath.value),
|
||||
set: (value: string) => {
|
||||
nacosContextPathCustomized.value = true;
|
||||
nacosContextPath.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
function resetNacosContextPathCustomization() {
|
||||
nacosContextPathCustomized.value = false;
|
||||
nacosContextPath.value = "";
|
||||
}
|
||||
|
||||
watch(nacosImplementation, (implementation) => {
|
||||
if (implementation === "rnacos") nacosVersionMode.value = "auto";
|
||||
if (implementation !== "rnacos") nacosHistoryEnabled.value = false;
|
||||
});
|
||||
|
||||
const colorOptions = [
|
||||
{ value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" },
|
||||
|
|
@ -1002,9 +1063,18 @@ watch(mqAuthKind, (kind) => {
|
|||
});
|
||||
|
||||
function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL;
|
||||
nacosImplementation.value = config?.implementation || (config?.rnacosConsoleAddr ? "rnacos" : "nacos");
|
||||
nacosVersionMode.value = config?.versionMode || "auto";
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || "";
|
||||
nacosNamespace.value = config?.namespace || "";
|
||||
nacosContextPath.value = config?.contextPath || "";
|
||||
nacosContextPathCustomized.value = !!config?.contextPath;
|
||||
nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || "";
|
||||
nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr;
|
||||
const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" };
|
||||
nacosConsoleAuthKind.value = consoleAuth.kind;
|
||||
nacosConsoleUsername.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.username : "";
|
||||
nacosConsolePassword.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.password : "";
|
||||
nacosTlsSkipVerify.value = !!config?.tlsSkipVerify;
|
||||
nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20;
|
||||
const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig;
|
||||
|
|
@ -1200,10 +1270,38 @@ function buildNacosAuth(): NacosAuthConfig {
|
|||
}
|
||||
|
||||
function buildNacosAdminConfig(): NacosAdminConfig {
|
||||
const primaryAddress = requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired"));
|
||||
const normalized = normalizeNacosEndpoint(primaryAddress, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
if (nacosImplementation.value === "rnacos" && normalized.warnings.length) {
|
||||
throw new Error(t("connection.nacosRNacosOpenApiRequired"));
|
||||
}
|
||||
let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined;
|
||||
if (nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value) {
|
||||
if (!nacosRNacosConsoleAddr.value.trim()) throw new Error(t("connection.nacosRNacosConsoleUrlRequired"));
|
||||
if (nacosConsoleAuthKind.value === "inherit") {
|
||||
if (nacosAuthKind.value !== "usernamePassword") throw new Error(t("connection.nacosConsoleAuthSeparateRequired"));
|
||||
rnacosConsoleAuth = { kind: "inherit" };
|
||||
} else {
|
||||
rnacosConsoleAuth = {
|
||||
kind: "usernamePassword",
|
||||
username: requireMqField(nacosConsoleUsername.value, t("connection.nacosConsoleUsernameRequired")),
|
||||
password: nacosConsolePassword.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
serverAddr: requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")),
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined,
|
||||
serverAddr: normalized.serverAddr,
|
||||
namespace: nacosNamespace.value.trim() || undefined,
|
||||
contextPath: nacosContextPath.value.trim(),
|
||||
contextPath: normalized.contextPath || undefined,
|
||||
rnacosConsoleAddr: nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value ? nacosRNacosConsoleAddr.value.trim() || undefined : undefined,
|
||||
rnacosHistoryEnabled: nacosImplementation.value === "rnacos" ? nacosHistoryEnabled.value : undefined,
|
||||
rnacosConsoleAuth,
|
||||
auth: buildNacosAuth(),
|
||||
tlsSkipVerify: nacosTlsSkipVerify.value || undefined,
|
||||
pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20,
|
||||
|
|
@ -1219,10 +1317,9 @@ function dockerNacosConsoleFallbackUrl(serverAddr: string): string | null {
|
|||
}
|
||||
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (port !== NACOS_LEGACY_SERVER_PORT || !["127.0.0.1", "localhost", "::1"].includes(host)) {
|
||||
return null;
|
||||
}
|
||||
parsed.port = NACOS_DOCKER_CONSOLE_PORT;
|
||||
if (port !== "8848" || !["127.0.0.1", "localhost", "::1"].includes(host)) return null;
|
||||
|
||||
parsed.port = "8085";
|
||||
return parsed.toString().replace(/\/$/, "");
|
||||
}
|
||||
|
||||
|
|
@ -1232,6 +1329,7 @@ function isNacosAdminEndpointNotFound(message: string): boolean {
|
|||
|
||||
async function tryNacosDockerConsoleFallback(config: ConnectionConfig, originalError: string, runId: number): Promise<SuccessfulConnectionTest | null> {
|
||||
if (config.db_type !== "nacos" || !isNacosAdminEndpointNotFound(originalError)) return null;
|
||||
|
||||
const fallbackUrl = dockerNacosConsoleFallbackUrl(nacosServerAddr.value);
|
||||
if (!fallbackUrl || fallbackUrl === nacosServerAddr.value.trim()) return null;
|
||||
|
||||
|
|
@ -1404,6 +1502,11 @@ async function setSqlServerDriverMode(mode: "auto" | "legacy") {
|
|||
}
|
||||
}
|
||||
|
||||
function isSqlServerTlsHandshakeFailure(message: string): boolean {
|
||||
const text = message.toLowerCase();
|
||||
return text.includes("sql server") && text.includes("tls") && (text.includes("handshake") || text.includes("eof") || text.includes("performing i/o"));
|
||||
}
|
||||
|
||||
function clearTestedConnectionInfo() {
|
||||
testedConfigFingerprint.value = "";
|
||||
testedConfigId.value = "";
|
||||
|
|
@ -2661,6 +2764,10 @@ async function testConnection() {
|
|||
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId);
|
||||
clearEditedConnectionErrorAfterSuccessfulTest();
|
||||
} else {
|
||||
const shouldShowSqlServerLegacyMode = config?.db_type === "sqlserver" && !sqlServerUsesLegacyCompatibility(config) && isSqlServerTlsHandshakeFailure(message);
|
||||
if (shouldShowSqlServerLegacyMode) {
|
||||
configTab.value = "advanced";
|
||||
}
|
||||
clearTestedConnectionInfo();
|
||||
testResult.value = { ok: false, message };
|
||||
showConnectionError(message);
|
||||
|
|
@ -4456,7 +4563,7 @@ function openExternalUrl(url: string) {
|
|||
|
||||
<TabsContent value="connection" class="m-0 min-h-0 flex-1 overflow-hidden">
|
||||
<div class="connection-form-body grid h-full min-h-0 gap-4 overflow-y-auto pt-4 pr-2">
|
||||
<div v-if="!isJdbcConnection" class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="!isJdbcConnection && form.db_type !== 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.connectionUrlOptional") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="connectionUrlInput" class="flex-1" :placeholder="connectionUrlPlaceholder" @keydown.enter.prevent="applyConnectionUrl" />
|
||||
|
|
@ -5018,24 +5125,92 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Nacos: server address, namespace and auth -->
|
||||
<!-- Nacos: profile-aware endpoint, namespace and auth -->
|
||||
<template v-else-if="form.db_type === 'nacos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosServerAddr" class="col-span-3" placeholder="http://127.0.0.1:8085" />
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosImplementation") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosImplementation === 'nacos' ? 'default' : 'outline'" @click="nacosImplementation = 'nacos'">Nacos</Button>
|
||||
<Button size="sm" :variant="nacosImplementation === 'rnacos' ? 'default' : 'outline'" @click="nacosImplementation = 'rnacos'">r-nacos</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosImplementation === 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosVersion") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" :variant="nacosVersionMode === 'auto' ? 'default' : 'outline'" @click="nacosVersionMode = 'auto'">{{ t("connection.nacosVersionAuto") }}</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v2' ? 'default' : 'outline'" @click="nacosVersionMode = 'v2'">2.x</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v3' ? 'default' : 'outline'" @click="nacosVersionMode = 'v3'">3.x</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ nacosPrimaryAddressLabel }}</Label>
|
||||
<Input v-model="nacosServerAddr" class="col-span-3" :placeholder="nacosPrimaryAddressPlaceholder" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosConsoleUrlHint") }}</p>
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosPrimaryAddressHint") }}</p>
|
||||
</div>
|
||||
<div v-if="nacosNormalizedPreview" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosRequestsUse", { address: nacosNormalizedPreview }) }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosContextPath") }}</Label>
|
||||
<div class="col-span-3 flex min-w-0 items-center gap-2">
|
||||
<Input v-model="nacosContextPathInput" class="min-w-0 flex-1" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
<Button v-if="nacosContextPathCustomized" type="button" size="sm" variant="ghost" @click="resetNacosContextPathCustomization">{{ t("connection.nacosContextPathRestoreAuto") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosNamespace") }}</Label>
|
||||
<Input v-model="nacosNamespace" class="col-span-3" placeholder="public" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosContextPath") }}</Label>
|
||||
<Input v-model="nacosContextPath" class="col-span-3" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
</div>
|
||||
<template v-if="nacosImplementation === 'rnacos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConfigurationHistory") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-2">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<Switch v-model="nacosHistoryEnabled" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.nacosConfigurationHistoryEnable") }}</span>
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" class="max-w-[360px] text-xs leading-relaxed">
|
||||
{{ t("connection.nacosConfigurationHistoryHint") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="nacosHistoryEnabled">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosRNacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosRNacosConsoleAddr" class="col-span-3" :placeholder="t('connection.nacosRNacosConsoleUrlPlaceholder')" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleAuthentication") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'inherit' ? 'default' : 'outline'" :disabled="nacosAuthKind === 'none'" @click="nacosConsoleAuthKind = 'inherit'">{{ t("connection.nacosConsoleAuthInherit") }}</Button>
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'usernamePassword' ? 'default' : 'outline'" @click="nacosConsoleAuthKind = 'usernamePassword'">{{ t("connection.nacosConsoleAuthSeparate") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosConsoleAuthKind === 'inherit' && nacosAuthKind === 'none'" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs text-destructive">{{ t("connection.nacosConsoleAuthPrimaryNone") }}</p>
|
||||
</div>
|
||||
<template v-if="nacosConsoleAuthKind === 'usernamePassword'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleUser") }}</Label
|
||||
><Input v-model="nacosConsoleUsername" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsolePassword") }}</Label
|
||||
><PasswordInput v-model="nacosConsolePassword" class="col-span-3" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosAuth") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import NacosConfigDiffDialog from "@/components/nacos/NacosConfigDiffDialog.vue";
|
||||
|
|
@ -85,6 +86,12 @@ const historyCompareLoading = ref(false);
|
|||
const historyCompareItem = ref<NacosConfigHistoryItem | null>(null);
|
||||
const pendingHistoryRollback = ref<NacosConfigHistoryItem | null>(null);
|
||||
const rollingBackHistory = ref(false);
|
||||
const rnacosConsoleAuthOpen = ref(false);
|
||||
const rnacosConsoleCaptchaImage = ref("");
|
||||
const rnacosConsoleCaptcha = ref("");
|
||||
const rnacosConsoleAuthError = ref("");
|
||||
const rnacosConsoleAuthLoading = ref(false);
|
||||
const rnacosConsoleRetryAction = shallowRef<(() => Promise<void>) | null>(null);
|
||||
const configFormatOptions = ["text", "json", "xml", "yaml", "html", "properties", "toml"];
|
||||
const configEditorHost = ref<HTMLDivElement | null>(null);
|
||||
const configEditorView = shallowRef<EditorView | null>(null);
|
||||
|
|
@ -117,6 +124,15 @@ const CONNECTION_NOT_FOUND_RETRY_DELAYS_MS = [150, 350, 700];
|
|||
const { gridTemplateColumns: configListGridTemplate, minWidth: configListMinWidth, resizingColumnIndex: configListResizingColumnIndex, onResizeStart: onConfigListColumnResizeStart } = useNacosConfigListColumnResize();
|
||||
|
||||
const namespace = computed(() => props.namespace ?? connectionInfo.value?.namespace ?? "");
|
||||
const supportsConfigHistory = computed(() => connectionInfo.value?.capabilities.supportsConfigHistory !== false);
|
||||
const configHistoryUnavailableTitle = computed(() => {
|
||||
if (supportsConfigHistory.value) return undefined;
|
||||
const reason = connectionInfo.value?.capabilities.historyUnavailableReason;
|
||||
if (reason === "historyDisabled") return t("nacos.historyDisabled");
|
||||
if (reason === "consoleUrlMissing") return t("nacos.historyConsoleUrlMissing");
|
||||
if (reason === "consoleCredentialsMissing") return t("nacos.historyConsoleCredentialsMissing");
|
||||
return t("nacos.historyUnavailable");
|
||||
});
|
||||
const namespaceLabel = computed(() => props.namespaceName || namespace.value || "public");
|
||||
const namespaceIdLabel = computed(() => {
|
||||
if (!namespace.value || namespace.value === namespaceLabel.value) return "";
|
||||
|
|
@ -537,7 +553,7 @@ function historyKeyFor(item: NacosConfigHistoryItem) {
|
|||
}
|
||||
|
||||
async function openConfigHistory() {
|
||||
if (!selectedConfigOriginalKey.value || !selectedConfig.value) return;
|
||||
if (!selectedConfigOriginalKey.value || !selectedConfig.value || !supportsConfigHistory.value) return;
|
||||
historyOpen.value = true;
|
||||
await loadConfigHistory(1);
|
||||
}
|
||||
|
|
@ -556,17 +572,81 @@ async function loadConfigHistory(page = historyPageNo.value) {
|
|||
historyItems.value = result.items;
|
||||
historyTotal.value = result.totalCount;
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => loadConfigHistory(historyPageNo.value));
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRNacosHistoryError(error: unknown, retryAction: () => Promise<void>) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("[rnacosConsoleCaptchaRequired]")) {
|
||||
historyError.value = message;
|
||||
return false;
|
||||
}
|
||||
rnacosConsoleRetryAction.value = retryAction;
|
||||
await requestRNacosConsoleAuthentication();
|
||||
return true;
|
||||
}
|
||||
|
||||
function retryRNacosConsoleAction() {
|
||||
const retryAction = rnacosConsoleRetryAction.value;
|
||||
rnacosConsoleRetryAction.value = null;
|
||||
// Run after the failed action has finished its own catch/finally cleanup;
|
||||
// otherwise that stale cleanup can close or clear the successfully retried UI.
|
||||
setTimeout(() => void (retryAction ? retryAction() : loadConfigHistory(historyPageNo.value)), 0);
|
||||
}
|
||||
|
||||
function rnacosCaptchaImageSource(image: string) {
|
||||
return image.startsWith("data:") ? image : `data:image/png;base64,${image}`;
|
||||
}
|
||||
|
||||
async function requestRNacosConsoleAuthentication() {
|
||||
rnacosConsoleAuthError.value = "";
|
||||
rnacosConsoleCaptcha.value = "";
|
||||
rnacosConsoleAuthLoading.value = true;
|
||||
try {
|
||||
const challenge = await api.nacosGetRNacosConsoleCaptcha(props.connectionId);
|
||||
if (!challenge.required) {
|
||||
await api.nacosLoginRNacosConsole(props.connectionId);
|
||||
void loadInfo();
|
||||
retryRNacosConsoleAction();
|
||||
return;
|
||||
}
|
||||
if (!challenge.image) throw new Error(t("nacos.rnacosCaptchaUnavailable"));
|
||||
rnacosConsoleCaptchaImage.value = rnacosCaptchaImageSource(challenge.image);
|
||||
rnacosConsoleAuthOpen.value = true;
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
rnacosConsoleAuthLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRNacosConsoleAuthentication() {
|
||||
if (!rnacosConsoleCaptcha.value.trim()) {
|
||||
rnacosConsoleAuthError.value = t("nacos.rnacosCaptchaRequired");
|
||||
return;
|
||||
}
|
||||
rnacosConsoleAuthLoading.value = true;
|
||||
rnacosConsoleAuthError.value = "";
|
||||
try {
|
||||
await api.nacosLoginRNacosConsole(props.connectionId, rnacosConsoleCaptcha.value);
|
||||
rnacosConsoleAuthOpen.value = false;
|
||||
void loadInfo();
|
||||
retryRNacosConsoleAction();
|
||||
} catch (error) {
|
||||
rnacosConsoleAuthError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
rnacosConsoleAuthLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistoryDetail(item: NacosConfigHistoryItem): Promise<NacosConfigItem | null> {
|
||||
try {
|
||||
return await api.nacosGetConfigHistory(props.connectionId, historyKeyFor(item));
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => viewConfigHistory(item));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -600,7 +680,7 @@ async function compareConfigHistory(item: NacosConfigHistoryItem) {
|
|||
historyCompareCurrent.value = current.content || "";
|
||||
historyCompareContent.value = history.content || "";
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => compareConfigHistory(item));
|
||||
historyCompareOpen.value = false;
|
||||
} finally {
|
||||
historyCompareLoading.value = false;
|
||||
|
|
@ -636,7 +716,7 @@ async function rollbackConfigHistory() {
|
|||
}
|
||||
await Promise.all([loadConfigs(configPageNo.value), loadConfigHistory(historyPageNo.value)]);
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => rollbackConfigHistory());
|
||||
} finally {
|
||||
rollingBackHistory.value = false;
|
||||
}
|
||||
|
|
@ -1028,10 +1108,12 @@ onBeforeUnmount(() => {
|
|||
<Download class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.export") }}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!selectedConfigOriginalKey" @click="openConfigHistory">
|
||||
<FileClock class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.history") }}
|
||||
</Button>
|
||||
<span class="inline-flex" :title="configHistoryUnavailableTitle">
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!selectedConfigOriginalKey || !supportsConfigHistory" @click="openConfigHistory">
|
||||
<FileClock class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.history") }}
|
||||
</Button>
|
||||
</span>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="readOnly" @click="saveConfigAsCopy">
|
||||
<Save class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.saveAs") }}
|
||||
|
|
@ -1188,6 +1270,30 @@ onBeforeUnmount(() => {
|
|||
@rollback="requestRollbackHistory"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="rnacosConsoleAuthOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.rnacosConsoleAuthTitle") }}</DialogTitle>
|
||||
<DialogDescription>{{ t("nacos.rnacosConsoleAuthDescription") }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3">
|
||||
<img v-if="rnacosConsoleCaptchaImage" :src="rnacosConsoleCaptchaImage" :alt="t('nacos.rnacosCaptchaLabel')" class="h-28 w-full rounded-md border bg-muted/30 object-contain" />
|
||||
<div class="space-y-1.5">
|
||||
<Label for="rnacos-console-captcha">{{ t("nacos.rnacosCaptchaLabel") }}</Label>
|
||||
<Input id="rnacos-console-captcha" v-model="rnacosConsoleCaptcha" autocomplete="off" :placeholder="t('nacos.rnacosCaptchaPlaceholder')" @keyup.enter="submitRNacosConsoleAuthentication" />
|
||||
</div>
|
||||
<p v-if="rnacosConsoleAuthError" class="text-xs text-destructive">{{ rnacosConsoleAuthError }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="rnacosConsoleAuthLoading" @click="requestRNacosConsoleAuthentication">{{ t("nacos.rnacosRefreshCaptcha") }}</Button>
|
||||
<Button :disabled="rnacosConsoleAuthLoading" @click="submitRNacosConsoleAuthentication">
|
||||
<Loader2 v-if="rnacosConsoleAuthLoading" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ t("nacos.rnacosConsoleAuthSubmit") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<NacosConfigDiffDialog
|
||||
v-model:open="historyCompareOpen"
|
||||
:title="t('nacos.historyCompareTitle')"
|
||||
|
|
|
|||
|
|
@ -314,16 +314,44 @@ export default {
|
|||
zookeeperCreateModeEphemeral: "Ephemeral",
|
||||
zookeeperCreateModePersistentSequential: "Persistent Sequential",
|
||||
zookeeperCreateModeEphemeralSequential: "Ephemeral Sequential",
|
||||
nacosConsoleUrl: "Console URL",
|
||||
nacosConsoleUrlHint: "Use the Nacos console/admin API address. Nacos 3 Docker usually exposes the console on 8085; older deployments may share 8848 with the service port.",
|
||||
nacosConsoleUrlRequired: "Nacos Console URL is required",
|
||||
nacosConsoleUrl: "Nacos management address",
|
||||
nacosConsoleUrlHint: "Use the address that matches the selected Nacos profile. Full browser and API URLs are accepted.",
|
||||
nacosConsoleUrlRequired: "Nacos management address is required",
|
||||
nacosConsoleUrlAutoAdjusted: "Adjusted Nacos Console URL from {from} to {to}.",
|
||||
nacosRNacosOpenApiAutoAdjusted: "Switched to the r-nacos OpenAPI: {from} → {to} (Context Path: /nacos).",
|
||||
nacosImplementation: "Service implementation",
|
||||
nacosVersion: "Nacos version",
|
||||
nacosVersionAuto: "Auto detect",
|
||||
nacosPrimaryAddressRNacos: "Nacos-compatible API address",
|
||||
nacosPrimaryAddressV2: "Service address (API and console shared)",
|
||||
nacosPrimaryAddressV3: "Admin API / console address",
|
||||
nacosPrimaryAddressAuto: "Nacos management address",
|
||||
nacosPrimaryAddressHint: "Paste a full browser or API URL. Known Nacos 3 console routes are normalized; custom proxy prefixes are preserved.",
|
||||
nacosRequestsUse: "Requests will use {address}/…",
|
||||
nacosNamespace: "Namespace",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPathPlaceholder: "Leave empty or /nacos",
|
||||
nacosContextPathRestoreAuto: "Restore automatic",
|
||||
nacosConfigurationHistory: "Configuration history",
|
||||
nacosConfigurationHistoryEnable: "Enable configuration history",
|
||||
nacosConfigurationHistoryHint:
|
||||
"r-nacos configuration history is available only through its independent console (normally port 10848). The console has a separate login session from the Nacos-compatible OpenAPI: the OpenAPI may not require authentication while the console still does. Use the same r-nacos user account unless you intentionally need a different one.",
|
||||
nacosRNacosConsoleUrl: "r-nacos Console URL",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "Optional. Required only for r-nacos configuration history; use the independent console URL (10848), not the OpenAPI URL.",
|
||||
nacosRNacosOpenApiRequired: "The r-nacos primary address must use the Nacos-compatible API, not the independent console URL.",
|
||||
nacosRNacosConsoleUrlRequired: "r-nacos configuration history requires a console URL.",
|
||||
nacosConsoleAuthSeparateRequired: "Primary authentication is None; configure separate console credentials.",
|
||||
nacosAuth: "Auth",
|
||||
nacosAuthNone: "None",
|
||||
nacosAuthUserPassword: "User / Password",
|
||||
nacosConsoleAuthentication: "Console authentication",
|
||||
nacosConsoleAuthInherit: "Use primary credentials",
|
||||
nacosConsoleAuthSeparate: "Separate credentials",
|
||||
nacosConsoleAuthPrimaryNone: "Primary authentication is None; use separate console credentials.",
|
||||
nacosConsoleUser: "Console user",
|
||||
nacosConsolePassword: "Console password",
|
||||
nacosConsoleUsernameRequired: "r-nacos console username is required",
|
||||
nacosUsernameRequired: "Nacos username is required",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "Skip certificate verification",
|
||||
|
|
@ -5428,6 +5456,18 @@ export default {
|
|||
exportFailed: "Export failed: {message}",
|
||||
history: "History",
|
||||
configHistory: "Config history",
|
||||
historyUnavailable: "Configuration history is unavailable for this connection. Configure the r-nacos console URL and reconnect to enable it.",
|
||||
historyDisabled: "Configuration history is disabled for this connection.",
|
||||
historyConsoleUrlMissing: "Configuration history needs an r-nacos console address.",
|
||||
historyConsoleCredentialsMissing: "Configuration history needs r-nacos console credentials.",
|
||||
rnacosConsoleAuthTitle: "r-nacos console verification",
|
||||
rnacosConsoleAuthDescription: "Enter the verification code from the r-nacos console to access configuration history.",
|
||||
rnacosCaptchaLabel: "Verification code",
|
||||
rnacosCaptchaPlaceholder: "Enter the code shown above",
|
||||
rnacosCaptchaRequired: "A verification code is required.",
|
||||
rnacosCaptchaUnavailable: "r-nacos did not return a verification image.",
|
||||
rnacosRefreshCaptcha: "Refresh code",
|
||||
rnacosConsoleAuthSubmit: "Verify and continue",
|
||||
historyDetail: "History detail",
|
||||
historyCompareTitle: "History Version Compare",
|
||||
currentPublishedContent: "Current published content:",
|
||||
|
|
|
|||
|
|
@ -302,16 +302,44 @@ export default withEnglishFallback({
|
|||
zookeeperCreateModeEphemeral: "Efímero",
|
||||
zookeeperCreateModePersistentSequential: "Persistente secuencial",
|
||||
zookeeperCreateModeEphemeralSequential: "Efímero secuencial",
|
||||
nacosConsoleUrl: "URL de consola",
|
||||
nacosConsoleUrlHint: "Usa la dirección de consola/admin API de Nacos. Nacos 3 Docker normalmente expone la consola en 8085; despliegues antiguos pueden compartir 8848 con el puerto de servicio.",
|
||||
nacosConsoleUrlRequired: "La URL de consola de Nacos es obligatoria",
|
||||
nacosConsoleUrl: "URL de OpenAPI / API de administración",
|
||||
nacosConsoleUrlHint: "Use la dirección correspondiente al perfil Nacos seleccionado. Se aceptan URL completas del navegador y de la API.",
|
||||
nacosConsoleUrlRequired: "La URL de OpenAPI / API de administración de Nacos es obligatoria",
|
||||
nacosConsoleUrlAutoAdjusted: "URL de consola de Nacos ajustada de {from} a {to}.",
|
||||
nacosRNacosOpenApiAutoAdjusted: "Se cambió automáticamente a la OpenAPI de r-nacos: {from} → {to} (Context Path: /nacos).",
|
||||
nacosImplementation: "Implementación del servicio",
|
||||
nacosVersion: "Versión de Nacos",
|
||||
nacosVersionAuto: "Detectar automáticamente",
|
||||
nacosPrimaryAddressRNacos: "Dirección de API compatible con Nacos",
|
||||
nacosPrimaryAddressV2: "Dirección del servicio (API y consola compartidas)",
|
||||
nacosPrimaryAddressV3: "Dirección de API de administración / consola",
|
||||
nacosPrimaryAddressAuto: "Dirección de administración de Nacos",
|
||||
nacosPrimaryAddressHint: "Pegue una URL completa del navegador o de la API. Las rutas conocidas de la consola de Nacos 3 se normalizan; se conservan los prefijos de proxy personalizados.",
|
||||
nacosRequestsUse: "Las solicitudes usarán {address}/…",
|
||||
nacosNamespace: "Namespace",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "Ruta de contexto",
|
||||
nacosContextPathPlaceholder: "Dejar vacío o /nacos",
|
||||
nacosContextPathRestoreAuto: "Restaurar automático",
|
||||
nacosConfigurationHistory: "Historial de configuración",
|
||||
nacosConfigurationHistoryEnable: "Habilitar historial de configuración",
|
||||
nacosConfigurationHistoryHint:
|
||||
"El historial de configuración de r-nacos solo está disponible mediante su consola independiente (normalmente el puerto 10848). La consola tiene una sesión de inicio de sesión distinta de la OpenAPI compatible con Nacos: la OpenAPI puede no requerir autenticación mientras que la consola sí. Use la misma cuenta de usuario de r-nacos salvo que necesite usar otra cuenta.",
|
||||
nacosRNacosConsoleUrl: "URL de consola r-nacos",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "Opcional. Solo se necesita para el historial de configuración de r-nacos; use la consola independiente (10848), no la URL de OpenAPI.",
|
||||
nacosRNacosOpenApiRequired: "La dirección principal de r-nacos debe usar la API compatible con Nacos, no la URL de la consola independiente.",
|
||||
nacosRNacosConsoleUrlRequired: "El historial de configuración de r-nacos requiere una URL de consola.",
|
||||
nacosConsoleAuthSeparateRequired: "La autenticación principal es Ninguna; configure credenciales de consola independientes.",
|
||||
nacosAuth: "Autenticación",
|
||||
nacosAuthNone: "Ninguna",
|
||||
nacosAuthUserPassword: "Usuario / Contraseña",
|
||||
nacosConsoleAuthentication: "Autenticación de consola",
|
||||
nacosConsoleAuthInherit: "Usar credenciales principales",
|
||||
nacosConsoleAuthSeparate: "Credenciales independientes",
|
||||
nacosConsoleAuthPrimaryNone: "La autenticación principal es Ninguna; use credenciales de consola independientes.",
|
||||
nacosConsoleUser: "Usuario de consola",
|
||||
nacosConsolePassword: "Contraseña de consola",
|
||||
nacosConsoleUsernameRequired: "El usuario de la consola r-nacos es obligatorio",
|
||||
nacosUsernameRequired: "El usuario de Nacos es obligatorio",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "Omitir verificación de certificado",
|
||||
|
|
@ -4209,6 +4237,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "Error al exportar: {message}",
|
||||
history: "History",
|
||||
configHistory: "Config history",
|
||||
historyUnavailable: "El historial de configuración no está disponible para esta conexión. Configure la URL de la consola r-nacos y vuelva a conectarse para habilitarlo.",
|
||||
historyDisabled: "El historial de configuración está deshabilitado para esta conexión.",
|
||||
historyConsoleUrlMissing: "El historial de configuración necesita una URL de consola r-nacos.",
|
||||
historyConsoleCredentialsMissing: "El historial de configuración necesita credenciales de consola r-nacos.",
|
||||
rnacosConsoleAuthTitle: "Verificación de consola r-nacos",
|
||||
rnacosConsoleAuthDescription: "Introduzca el código de verificación de la consola r-nacos para acceder al historial.",
|
||||
rnacosCaptchaLabel: "Código de verificación",
|
||||
rnacosCaptchaPlaceholder: "Introduzca el código mostrado arriba",
|
||||
rnacosCaptchaRequired: "Se requiere un código de verificación.",
|
||||
rnacosCaptchaUnavailable: "r-nacos no devolvió una imagen de verificación.",
|
||||
rnacosRefreshCaptcha: "Actualizar código",
|
||||
rnacosConsoleAuthSubmit: "Verificar y continuar",
|
||||
historyDetail: "History detail",
|
||||
historyCompareTitle: "History Version Compare",
|
||||
currentPublishedContent: "Current published content:",
|
||||
|
|
|
|||
|
|
@ -301,16 +301,44 @@ export default withEnglishFallback({
|
|||
zookeeperCreateModeEphemeral: "Effimero",
|
||||
zookeeperCreateModePersistentSequential: "Persistente Sequenziale",
|
||||
zookeeperCreateModeEphemeralSequential: "Effimero Sequenziale",
|
||||
nacosConsoleUrl: "URL console",
|
||||
nacosConsoleUrlHint: "Usa l'indirizzo console/admin API di Nacos. Nacos 3 Docker di solito espone la console su 8085; distribuzioni più vecchie possono condividere 8848 con la porta di servizio.",
|
||||
nacosConsoleUrlRequired: "L'URL console Nacos è obbligatorio",
|
||||
nacosConsoleUrl: "URL OpenAPI / API di amministrazione",
|
||||
nacosConsoleUrlHint: "Usa l'indirizzo corrispondente al profilo Nacos selezionato. Sono accettati URL completi del browser e dell'API.",
|
||||
nacosConsoleUrlRequired: "L'URL OpenAPI / API di amministrazione Nacos è obbligatorio",
|
||||
nacosConsoleUrlAutoAdjusted: "URL console Nacos regolato da {from} a {to}.",
|
||||
nacosRNacosOpenApiAutoAdjusted: "Passaggio automatico alla OpenAPI r-nacos: {from} → {to} (Context Path: /nacos).",
|
||||
nacosImplementation: "Implementazione del servizio",
|
||||
nacosVersion: "Versione Nacos",
|
||||
nacosVersionAuto: "Rileva automaticamente",
|
||||
nacosPrimaryAddressRNacos: "Indirizzo API compatibile con Nacos",
|
||||
nacosPrimaryAddressV2: "Indirizzo del servizio (API e console condivise)",
|
||||
nacosPrimaryAddressV3: "Indirizzo API di amministrazione / console",
|
||||
nacosPrimaryAddressAuto: "Indirizzo di amministrazione Nacos",
|
||||
nacosPrimaryAddressHint: "Incolla un URL completo del browser o dell'API. I percorsi noti della console Nacos 3 vengono normalizzati; i prefissi proxy personalizzati vengono mantenuti.",
|
||||
nacosRequestsUse: "Le richieste useranno {address}/…",
|
||||
nacosNamespace: "Namespace",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "Percorso di contesto",
|
||||
nacosContextPathPlaceholder: "Lascia vuoto o /nacos",
|
||||
nacosContextPathRestoreAuto: "Ripristina automatico",
|
||||
nacosConfigurationHistory: "Cronologia di configurazione",
|
||||
nacosConfigurationHistoryEnable: "Abilita cronologia di configurazione",
|
||||
nacosConfigurationHistoryHint:
|
||||
"La cronologia di configurazione di r-nacos è disponibile solo tramite la console separata (normalmente sulla porta 10848). La console usa una sessione di accesso distinta dall'OpenAPI compatibile con Nacos: l'OpenAPI potrebbe non richiedere l'autenticazione, mentre la console sì. Usa lo stesso account utente r-nacos, salvo che sia necessario usarne uno diverso.",
|
||||
nacosRNacosConsoleUrl: "URL console r-nacos",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "Facoltativo. Necessario solo per la cronologia delle configurazioni r-nacos; usare la console separata (10848), non l'URL OpenAPI.",
|
||||
nacosRNacosOpenApiRequired: "L'indirizzo principale di r-nacos deve usare l'API compatibile con Nacos, non l'URL della console separata.",
|
||||
nacosRNacosConsoleUrlRequired: "La cronologia delle configurazioni r-nacos richiede un URL della console.",
|
||||
nacosConsoleAuthSeparateRequired: "L'autenticazione principale è Nessuna; configura credenziali della console separate.",
|
||||
nacosAuth: "Autenticazione",
|
||||
nacosAuthNone: "Nessuna",
|
||||
nacosAuthUserPassword: "Utente / Password",
|
||||
nacosConsoleAuthentication: "Autenticazione console",
|
||||
nacosConsoleAuthInherit: "Usa credenziali principali",
|
||||
nacosConsoleAuthSeparate: "Credenziali separate",
|
||||
nacosConsoleAuthPrimaryNone: "L'autenticazione principale è Nessuna; usa credenziali console separate.",
|
||||
nacosConsoleUser: "Utente console",
|
||||
nacosConsolePassword: "Password console",
|
||||
nacosConsoleUsernameRequired: "Il nome utente della console r-nacos è obbligatorio",
|
||||
nacosUsernameRequired: "Il nome utente Nacos è obbligatorio",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "Salta verifica certificato",
|
||||
|
|
@ -4207,6 +4235,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "Esportazione non riuscita: {message}",
|
||||
history: "History",
|
||||
configHistory: "Config history",
|
||||
historyUnavailable: "La cronologia delle configurazioni non è disponibile per questa connessione. Configura l'URL della console r-nacos e riconnettiti per abilitarla.",
|
||||
historyDisabled: "La cronologia delle configurazioni è disabilitata per questa connessione.",
|
||||
historyConsoleUrlMissing: "La cronologia delle configurazioni richiede un URL della console r-nacos.",
|
||||
historyConsoleCredentialsMissing: "La cronologia delle configurazioni richiede le credenziali della console r-nacos.",
|
||||
rnacosConsoleAuthTitle: "Verifica console r-nacos",
|
||||
rnacosConsoleAuthDescription: "Inserisci il codice di verifica della console r-nacos per accedere alla cronologia.",
|
||||
rnacosCaptchaLabel: "Codice di verifica",
|
||||
rnacosCaptchaPlaceholder: "Inserisci il codice mostrato sopra",
|
||||
rnacosCaptchaRequired: "È richiesto un codice di verifica.",
|
||||
rnacosCaptchaUnavailable: "r-nacos non ha restituito un'immagine di verifica.",
|
||||
rnacosRefreshCaptcha: "Aggiorna codice",
|
||||
rnacosConsoleAuthSubmit: "Verifica e continua",
|
||||
historyDetail: "History detail",
|
||||
historyCompareTitle: "History Version Compare",
|
||||
currentPublishedContent: "Current published content:",
|
||||
|
|
|
|||
|
|
@ -295,16 +295,44 @@ export default withEnglishFallback({
|
|||
etcdClientCertBrowse: "クライアント証明書を選択",
|
||||
etcdClientKeyBrowse: "クライアント秘密鍵を選択",
|
||||
etcdClientCertPairRequired: "クライアント証明書と秘密鍵の両方を指定する必要があります。",
|
||||
nacosConsoleUrl: "コンソールURL",
|
||||
nacosConsoleUrlHint: "Nacosのコンソール/admin APIアドレスを指定します。Nacos 3 Dockerでは通常8085、古い構成ではサービス用ポート8848と共用される場合があります。",
|
||||
nacosConsoleUrlRequired: "NacosコンソールURLは必須です",
|
||||
nacosConsoleUrl: "OpenAPI / 管理 API URL",
|
||||
nacosConsoleUrlHint: "選択した Nacos プロファイルに対応するアドレスを指定します。完全なブラウザー URL と API URL を使用できます。",
|
||||
nacosConsoleUrlRequired: "Nacos OpenAPI / 管理 API URL は必須です",
|
||||
nacosConsoleUrlAutoAdjusted: "NacosコンソールURLを{from}から{to}に自動調整しました。",
|
||||
nacosRNacosOpenApiAutoAdjusted: "r-nacos OpenAPI に自動切り替えました: {from} → {to}(Context Path: /nacos)。",
|
||||
nacosImplementation: "サービス実装",
|
||||
nacosVersion: "Nacos バージョン",
|
||||
nacosVersionAuto: "自動検出",
|
||||
nacosPrimaryAddressRNacos: "Nacos 互換 API アドレス",
|
||||
nacosPrimaryAddressV2: "サービスアドレス(API とコンソールで共有)",
|
||||
nacosPrimaryAddressV3: "管理 API / コンソールアドレス",
|
||||
nacosPrimaryAddressAuto: "Nacos 管理アドレス",
|
||||
nacosPrimaryAddressHint: "完全なブラウザーまたは API URL を貼り付けてください。既知の Nacos 3 コンソールルートは正規化され、カスタムプロキシプレフィックスは保持されます。",
|
||||
nacosRequestsUse: "リクエストには {address}/… を使用します",
|
||||
nacosNamespace: "名前空間",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "コンテキストパス",
|
||||
nacosContextPathPlaceholder: "空欄または /nacos",
|
||||
nacosContextPathRestoreAuto: "自動に戻す",
|
||||
nacosConfigurationHistory: "設定履歴",
|
||||
nacosConfigurationHistoryEnable: "設定履歴を有効にする",
|
||||
nacosConfigurationHistoryHint:
|
||||
"r-nacos の設定履歴は、独立コンソール(通常はポート 10848)からのみ利用できます。コンソールと Nacos 互換 OpenAPI は別のログインセッションを使用するため、OpenAPI の認証が無効でもコンソールへのログインが必要になる場合があります。通常は同じ r-nacos ユーザーアカウントを使用し、別のアカウントが必要な場合のみ個別に設定してください。",
|
||||
nacosRNacosConsoleUrl: "r-nacos コンソール URL",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "任意。r-nacos の設定履歴にのみ必要です。OpenAPI URL ではなく、独立コンソール(10848)を指定してください。",
|
||||
nacosRNacosOpenApiRequired: "r-nacos のメインアドレスには、独立コンソール URL ではなく Nacos 互換 API アドレスを使用してください。",
|
||||
nacosRNacosConsoleUrlRequired: "r-nacos の設定履歴にはコンソール URL が必要です。",
|
||||
nacosConsoleAuthSeparateRequired: "メイン接続の認証が未設定です。コンソール用の認証情報を別途設定してください。",
|
||||
nacosAuth: "認証",
|
||||
nacosAuthNone: "なし",
|
||||
nacosAuthUserPassword: "ユーザー / パスワード",
|
||||
nacosConsoleAuthentication: "コンソール認証",
|
||||
nacosConsoleAuthInherit: "メイン接続の認証情報を使用",
|
||||
nacosConsoleAuthSeparate: "認証情報を別途設定",
|
||||
nacosConsoleAuthPrimaryNone: "メイン接続の認証は未設定です。コンソール用の認証情報を別途設定してください。",
|
||||
nacosConsoleUser: "コンソールユーザー",
|
||||
nacosConsolePassword: "コンソールパスワード",
|
||||
nacosConsoleUsernameRequired: "r-nacos コンソールユーザー名は必須です",
|
||||
nacosUsernameRequired: "Nacosユーザー名は必須です",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "証明書検証をスキップ",
|
||||
|
|
@ -4195,6 +4223,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "エクスポートに失敗しました: {message}",
|
||||
history: "History",
|
||||
configHistory: "Config history",
|
||||
historyUnavailable: "この接続では設定履歴を利用できません。r-nacos コンソール URL を設定して再接続してください。",
|
||||
historyDisabled: "この接続では設定履歴が無効になっています。",
|
||||
historyConsoleUrlMissing: "設定履歴を使用するには r-nacos コンソール URL が必要です。",
|
||||
historyConsoleCredentialsMissing: "設定履歴を使用するには r-nacos コンソールの認証情報が必要です。",
|
||||
rnacosConsoleAuthTitle: "r-nacos コンソール認証",
|
||||
rnacosConsoleAuthDescription: "設定履歴にアクセスするには、r-nacos コンソールの認証コードを入力してください。",
|
||||
rnacosCaptchaLabel: "認証コード",
|
||||
rnacosCaptchaPlaceholder: "上の画像に表示されたコードを入力",
|
||||
rnacosCaptchaRequired: "認証コードを入力してください。",
|
||||
rnacosCaptchaUnavailable: "r-nacos から認証画像が返されませんでした。",
|
||||
rnacosRefreshCaptcha: "コードを更新",
|
||||
rnacosConsoleAuthSubmit: "認証して続行",
|
||||
historyDetail: "History detail",
|
||||
historyCompareTitle: "History Version Compare",
|
||||
currentPublishedContent: "Current published content:",
|
||||
|
|
|
|||
|
|
@ -302,16 +302,44 @@ export default withEnglishFallback({
|
|||
zookeeperCreateModeEphemeral: "Efêmero",
|
||||
zookeeperCreateModePersistentSequential: "Persistente Sequencial",
|
||||
zookeeperCreateModeEphemeralSequential: "Efêmero Sequencial",
|
||||
nacosConsoleUrl: "URL do console",
|
||||
nacosConsoleUrlHint: "Use o endereço do console/admin API do Nacos. O Nacos 3 Docker geralmente expõe o console em 8085; implantações antigas podem compartilhar 8848 com a porta de serviço.",
|
||||
nacosConsoleUrlRequired: "A URL do console Nacos é obrigatória",
|
||||
nacosConsoleUrl: "URL da OpenAPI / API administrativa",
|
||||
nacosConsoleUrlHint: "Use o endereço correspondente ao perfil Nacos selecionado. URLs completas do navegador e da API são aceitas.",
|
||||
nacosConsoleUrlRequired: "A URL da OpenAPI / API administrativa do Nacos é obrigatória",
|
||||
nacosConsoleUrlAutoAdjusted: "URL do console Nacos ajustada de {from} para {to}.",
|
||||
nacosRNacosOpenApiAutoAdjusted: "Alterado automaticamente para a OpenAPI do r-nacos: {from} → {to} (Context Path: /nacos).",
|
||||
nacosImplementation: "Implementação do serviço",
|
||||
nacosVersion: "Versão do Nacos",
|
||||
nacosVersionAuto: "Detectar automaticamente",
|
||||
nacosPrimaryAddressRNacos: "Endereço de API compatível com Nacos",
|
||||
nacosPrimaryAddressV2: "Endereço do serviço (API e console compartilhados)",
|
||||
nacosPrimaryAddressV3: "Endereço de API administrativa / console",
|
||||
nacosPrimaryAddressAuto: "Endereço de administração do Nacos",
|
||||
nacosPrimaryAddressHint: "Cole uma URL completa do navegador ou da API. As rotas conhecidas do console do Nacos 3 são normalizadas; prefixos de proxy personalizados são preservados.",
|
||||
nacosRequestsUse: "As solicitações usarão {address}/…",
|
||||
nacosNamespace: "Namespace",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "Caminho de contexto",
|
||||
nacosContextPathPlaceholder: "Deixe vazio ou /nacos",
|
||||
nacosContextPathRestoreAuto: "Restaurar automático",
|
||||
nacosConfigurationHistory: "Histórico de configuração",
|
||||
nacosConfigurationHistoryEnable: "Ativar histórico de configuração",
|
||||
nacosConfigurationHistoryHint:
|
||||
"O histórico de configuração do r-nacos está disponível apenas pelo console independente (normalmente na porta 10848). O console usa uma sessão de login separada da OpenAPI compatível com Nacos: a OpenAPI pode não exigir autenticação, enquanto o console ainda exige. Use a mesma conta de usuário do r-nacos, a menos que você precise usar outra conta.",
|
||||
nacosRNacosConsoleUrl: "URL do console r-nacos",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "Opcional. Necessária apenas para o histórico de configurações do r-nacos; use o console independente (10848), não a URL OpenAPI.",
|
||||
nacosRNacosOpenApiRequired: "O endereço principal do r-nacos deve usar a API compatível com Nacos, não a URL do console independente.",
|
||||
nacosRNacosConsoleUrlRequired: "O histórico de configurações do r-nacos exige uma URL do console.",
|
||||
nacosConsoleAuthSeparateRequired: "A autenticação principal é Nenhuma; configure credenciais de console separadas.",
|
||||
nacosAuth: "Autenticação",
|
||||
nacosAuthNone: "Nenhuma",
|
||||
nacosAuthUserPassword: "Usuário / Senha",
|
||||
nacosConsoleAuthentication: "Autenticação do console",
|
||||
nacosConsoleAuthInherit: "Usar credenciais principais",
|
||||
nacosConsoleAuthSeparate: "Credenciais separadas",
|
||||
nacosConsoleAuthPrimaryNone: "A autenticação principal é Nenhuma; use credenciais de console separadas.",
|
||||
nacosConsoleUser: "Usuário do console",
|
||||
nacosConsolePassword: "Senha do console",
|
||||
nacosConsoleUsernameRequired: "O usuário do console r-nacos é obrigatório",
|
||||
nacosUsernameRequired: "O usuário Nacos é obrigatório",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "Ignorar verificação do certificado",
|
||||
|
|
@ -4209,6 +4237,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "Falha ao exportar: {message}",
|
||||
history: "History",
|
||||
configHistory: "Config history",
|
||||
historyUnavailable: "O histórico de configurações não está disponível para esta conexão. Configure a URL do console r-nacos e reconecte para habilitá-lo.",
|
||||
historyDisabled: "O histórico de configurações está desabilitado para esta conexão.",
|
||||
historyConsoleUrlMissing: "O histórico de configurações precisa de uma URL do console r-nacos.",
|
||||
historyConsoleCredentialsMissing: "O histórico de configurações precisa das credenciais do console r-nacos.",
|
||||
rnacosConsoleAuthTitle: "Verificação do console r-nacos",
|
||||
rnacosConsoleAuthDescription: "Informe o código de verificação do console r-nacos para acessar o histórico.",
|
||||
rnacosCaptchaLabel: "Código de verificação",
|
||||
rnacosCaptchaPlaceholder: "Informe o código exibido acima",
|
||||
rnacosCaptchaRequired: "É necessário informar um código de verificação.",
|
||||
rnacosCaptchaUnavailable: "O r-nacos não retornou uma imagem de verificação.",
|
||||
rnacosRefreshCaptcha: "Atualizar código",
|
||||
rnacosConsoleAuthSubmit: "Verificar e continuar",
|
||||
historyDetail: "History detail",
|
||||
historyCompareTitle: "History Version Compare",
|
||||
currentPublishedContent: "Current published content:",
|
||||
|
|
|
|||
|
|
@ -316,16 +316,43 @@ export default withEnglishFallback({
|
|||
zookeeperCreateModeEphemeral: "临时节点",
|
||||
zookeeperCreateModePersistentSequential: "持久顺序节点",
|
||||
zookeeperCreateModeEphemeralSequential: "临时顺序节点",
|
||||
nacosConsoleUrl: "控制台 URL",
|
||||
nacosConsoleUrlHint: "填写 Nacos 控制台/admin API 地址。Nacos 3 Docker 通常是 8085;如果旧版控制台和服务共用 8848,也可以填 8848。",
|
||||
nacosConsoleUrlRequired: "Nacos 控制台 URL 不能为空",
|
||||
nacosConsoleUrl: "OpenAPI / 管理 API URL",
|
||||
nacosConsoleUrlHint: "填写与所选 Nacos 配置匹配的地址,支持完整的浏览器或 API URL。",
|
||||
nacosConsoleUrlRequired: "Nacos OpenAPI / 管理 API URL 不能为空",
|
||||
nacosConsoleUrlAutoAdjusted: "已自动将 Nacos 控制台 URL 从 {from} 调整为 {to}。",
|
||||
nacosRNacosOpenApiAutoAdjusted: "已自动改用 r-nacos OpenAPI:{from} → {to}(Context Path: /nacos)。",
|
||||
nacosImplementation: "服务实现",
|
||||
nacosVersion: "Nacos 版本",
|
||||
nacosVersionAuto: "自动检测",
|
||||
nacosPrimaryAddressRNacos: "兼容 Nacos 的 API 地址",
|
||||
nacosPrimaryAddressV2: "服务地址(API 与控制台共用)",
|
||||
nacosPrimaryAddressV3: "管理 API / 控制台地址",
|
||||
nacosPrimaryAddressAuto: "Nacos 管理地址",
|
||||
nacosPrimaryAddressHint: "可粘贴完整的浏览器或 API URL。已知的 Nacos 3 控制台路径会自动规范化;自定义代理前缀会被保留。",
|
||||
nacosRequestsUse: "请求将使用 {address}/…",
|
||||
nacosNamespace: "命名空间",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "上下文路径",
|
||||
nacosContextPathPlaceholder: "留空或填写 /nacos",
|
||||
nacosContextPathRestoreAuto: "恢复自动",
|
||||
nacosConfigurationHistory: "配置历史",
|
||||
nacosConfigurationHistoryEnable: "启用配置历史",
|
||||
nacosConfigurationHistoryHint: "r-nacos 的配置历史仅通过独立控制台提供(通常为 10848 端口)。控制台与兼容 Nacos 的 OpenAPI 使用不同的登录会话:OpenAPI 可以不启用认证,而控制台仍需登录。通常填写同一套 r-nacos 用户凭据;仅在需要使用另一账号时单独设置。",
|
||||
nacosRNacosConsoleUrl: "r-nacos 控制台 URL",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "可选。仅 r-nacos 的配置历史需要填写:使用独立控制台服务地址(10848),不是 OpenAPI 地址。",
|
||||
nacosRNacosOpenApiRequired: "r-nacos 主地址必须使用兼容 Nacos 的 API 地址,不能使用独立控制台 URL。",
|
||||
nacosRNacosConsoleUrlRequired: "启用 r-nacos 配置历史时必须填写控制台 URL。",
|
||||
nacosConsoleAuthSeparateRequired: "主连接未启用认证;请单独设置控制台凭据。",
|
||||
nacosAuth: "认证",
|
||||
nacosAuthNone: "无",
|
||||
nacosAuthUserPassword: "用户名 / 密码",
|
||||
nacosConsoleAuthentication: "控制台认证",
|
||||
nacosConsoleAuthInherit: "使用主连接凭据",
|
||||
nacosConsoleAuthSeparate: "单独设置凭据",
|
||||
nacosConsoleAuthPrimaryNone: "主连接未启用认证;请单独设置控制台凭据。",
|
||||
nacosConsoleUser: "控制台用户名",
|
||||
nacosConsolePassword: "控制台密码",
|
||||
nacosConsoleUsernameRequired: "r-nacos 控制台用户名不能为空",
|
||||
nacosUsernameRequired: "Nacos 用户名不能为空",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "跳过证书验证",
|
||||
|
|
@ -5416,6 +5443,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "导出失败:{message}",
|
||||
history: "历史",
|
||||
configHistory: "配置历史",
|
||||
historyUnavailable: "当前连接无法使用配置历史。请配置 r-nacos 控制台 URL 并重新连接后再试。",
|
||||
historyDisabled: "当前连接已禁用配置历史。",
|
||||
historyConsoleUrlMissing: "配置历史需要填写 r-nacos 控制台地址。",
|
||||
historyConsoleCredentialsMissing: "配置历史需要填写 r-nacos 控制台凭据。",
|
||||
rnacosConsoleAuthTitle: "r-nacos 控制台验证",
|
||||
rnacosConsoleAuthDescription: "请输入 r-nacos 控制台验证码,以访问配置历史。",
|
||||
rnacosCaptchaLabel: "验证码",
|
||||
rnacosCaptchaPlaceholder: "输入上图中的验证码",
|
||||
rnacosCaptchaRequired: "请输入验证码。",
|
||||
rnacosCaptchaUnavailable: "r-nacos 未返回验证码图片。",
|
||||
rnacosRefreshCaptcha: "刷新验证码",
|
||||
rnacosConsoleAuthSubmit: "验证并继续",
|
||||
historyDetail: "历史版本详情",
|
||||
historyCompareTitle: "历史版本对比",
|
||||
currentPublishedContent: "当前已发布内容:",
|
||||
|
|
|
|||
|
|
@ -302,16 +302,43 @@ export default withEnglishFallback({
|
|||
zookeeperCreateModeEphemeral: "臨時",
|
||||
zookeeperCreateModePersistentSequential: "持久有序",
|
||||
zookeeperCreateModeEphemeralSequential: "臨時有序",
|
||||
nacosConsoleUrl: "控制台 URL",
|
||||
nacosConsoleUrlHint: "填寫 Nacos 控制台/admin API 位址。Nacos 3 Docker 通常是 8085;如果舊版控制台和服務共用 8848,也可以填 8848。",
|
||||
nacosConsoleUrlRequired: "Nacos 控制台 URL 不能為空",
|
||||
nacosConsoleUrl: "OpenAPI / 管理 API URL",
|
||||
nacosConsoleUrlHint: "填寫與所選 Nacos 設定相符的位址,支援完整的瀏覽器或 API URL。",
|
||||
nacosConsoleUrlRequired: "Nacos OpenAPI / 管理 API URL 不能為空",
|
||||
nacosConsoleUrlAutoAdjusted: "已自動將 Nacos 控制台 URL 從 {from} 調整為 {to}。",
|
||||
nacosRNacosOpenApiAutoAdjusted: "已自動改用 r-nacos OpenAPI:{from} → {to}(Context Path: /nacos)。",
|
||||
nacosImplementation: "服務實作",
|
||||
nacosVersion: "Nacos 版本",
|
||||
nacosVersionAuto: "自動偵測",
|
||||
nacosPrimaryAddressRNacos: "相容 Nacos 的 API 位址",
|
||||
nacosPrimaryAddressV2: "服務位址(API 與主控台共用)",
|
||||
nacosPrimaryAddressV3: "管理 API / 主控台位址",
|
||||
nacosPrimaryAddressAuto: "Nacos 管理位址",
|
||||
nacosPrimaryAddressHint: "可貼上完整的瀏覽器或 API URL。已知的 Nacos 3 主控台路徑會自動正規化;自訂代理前綴會被保留。",
|
||||
nacosRequestsUse: "請求將使用 {address}/…",
|
||||
nacosNamespace: "命名空間",
|
||||
nacosContextPath: "Context Path",
|
||||
nacosContextPath: "上下文路徑",
|
||||
nacosContextPathPlaceholder: "留空或填寫 /nacos",
|
||||
nacosContextPathRestoreAuto: "恢復自動",
|
||||
nacosConfigurationHistory: "設定歷史",
|
||||
nacosConfigurationHistoryEnable: "啟用設定歷史",
|
||||
nacosConfigurationHistoryHint: "r-nacos 的設定歷史僅能透過獨立主控台使用(通常為 10848 連接埠)。主控台與相容 Nacos 的 OpenAPI 使用不同的登入工作階段:OpenAPI 可以不啟用驗證,但主控台仍需要登入。通常填寫同一套 r-nacos 使用者憑證;僅在需要使用另一個帳號時個別設定。",
|
||||
nacosRNacosConsoleUrl: "r-nacos 控制台 URL",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "選填。僅 r-nacos 的設定歷史需要填寫:使用獨立控制台服務位址(10848),不是 OpenAPI 位址。",
|
||||
nacosRNacosOpenApiRequired: "r-nacos 主要位址必須使用相容 Nacos 的 API 位址,不能使用獨立控制台 URL。",
|
||||
nacosRNacosConsoleUrlRequired: "啟用 r-nacos 設定歷史時必須填寫控制台 URL。",
|
||||
nacosConsoleAuthSeparateRequired: "主要連線未啟用驗證;請個別設定控制台憑證。",
|
||||
nacosAuth: "認證",
|
||||
nacosAuthNone: "無",
|
||||
nacosAuthUserPassword: "使用者名稱 / 密碼",
|
||||
nacosConsoleAuthentication: "主控台驗證",
|
||||
nacosConsoleAuthInherit: "使用主要連線憑證",
|
||||
nacosConsoleAuthSeparate: "個別設定憑證",
|
||||
nacosConsoleAuthPrimaryNone: "主要連線未啟用驗證;請個別設定主控台憑證。",
|
||||
nacosConsoleUser: "主控台使用者名稱",
|
||||
nacosConsolePassword: "主控台密碼",
|
||||
nacosConsoleUsernameRequired: "r-nacos 主控台使用者名稱不能為空",
|
||||
nacosUsernameRequired: "Nacos 使用者名稱不能為空",
|
||||
nacosTls: "TLS",
|
||||
nacosTlsSkipVerify: "跳過憑證驗證",
|
||||
|
|
@ -4012,6 +4039,18 @@ export default withEnglishFallback({
|
|||
exportFailed: "匯出失敗:{message}",
|
||||
history: "歷史",
|
||||
configHistory: "配置歷史",
|
||||
historyUnavailable: "目前連線無法使用設定歷史。請設定 r-nacos 控制台 URL 並重新連線後再試。",
|
||||
historyDisabled: "目前連線已停用設定歷史。",
|
||||
historyConsoleUrlMissing: "設定歷史需要填寫 r-nacos 控制台位址。",
|
||||
historyConsoleCredentialsMissing: "設定歷史需要填寫 r-nacos 控制台憑證。",
|
||||
rnacosConsoleAuthTitle: "r-nacos 控制台驗證",
|
||||
rnacosConsoleAuthDescription: "請輸入 r-nacos 控制台驗證碼以存取設定歷史。",
|
||||
rnacosCaptchaLabel: "驗證碼",
|
||||
rnacosCaptchaPlaceholder: "輸入上圖中的驗證碼",
|
||||
rnacosCaptchaRequired: "請輸入驗證碼。",
|
||||
rnacosCaptchaUnavailable: "r-nacos 未傳回驗證碼圖片。",
|
||||
rnacosRefreshCaptcha: "重新整理驗證碼",
|
||||
rnacosConsoleAuthSubmit: "驗證並繼續",
|
||||
historyDetail: "歷史版本詳情",
|
||||
historyCompareTitle: "歷史版本對比",
|
||||
currentPublishedContent: "目前已發布內容:",
|
||||
|
|
|
|||
|
|
@ -10,12 +10,31 @@ import {
|
|||
nacosConfigFileExtension,
|
||||
parseNacosRawBody,
|
||||
parseNacosRawQuery,
|
||||
normalizeNacosEndpoint,
|
||||
resolveRNacosOpenApiFallback,
|
||||
resolveNacosConfigCopyText,
|
||||
sanitizeNacosConfigFileNameSegment,
|
||||
summarizeNacosConfigDiff,
|
||||
} from "@/lib/nacos/nacosAdmin";
|
||||
|
||||
describe("nacosAdmin helpers", () => {
|
||||
it("normalizes Nacos profile URLs without losing proxy prefixes", () => {
|
||||
expect(normalizeNacosEndpoint("https://[2001:db8::1]:9443/gateway/nacos/", { implementation: "nacos", versionMode: "v2" })).toMatchObject({
|
||||
serverAddr: "https://[2001:db8::1]:9443",
|
||||
contextPath: "/gateway/nacos",
|
||||
detectedVersion: "v2",
|
||||
});
|
||||
expect(normalizeNacosEndpoint("https://nacos.example/gateway/next/index.html", { implementation: "nacos", versionMode: "v3" })).toMatchObject({
|
||||
serverAddr: "https://nacos.example",
|
||||
contextPath: "/gateway",
|
||||
detectedVersion: "v3",
|
||||
});
|
||||
expect(normalizeNacosEndpoint("http://rnacos.example:8848/nacos", { implementation: "rnacos" })).toMatchObject({
|
||||
serverAddr: "http://rnacos.example:8848",
|
||||
contextPath: "/nacos",
|
||||
});
|
||||
expect(() => normalizeNacosEndpoint("http://user:secret@nacos.example", { implementation: "nacos" })).toThrow(/embedded credentials/i);
|
||||
});
|
||||
it("parses raw query and body text", () => {
|
||||
expect(parseNacosRawQuery("?dataId=a&group=DEFAULT_GROUP")).toEqual({ dataId: "a", group: "DEFAULT_GROUP" });
|
||||
expect(parseNacosRawQuery("")).toBeUndefined();
|
||||
|
|
@ -30,6 +49,23 @@ describe("nacosAdmin helpers", () => {
|
|||
expect(isNacosRawMutation("DELETE")).toBe(true);
|
||||
});
|
||||
|
||||
it("redirects r-nacos console settings to the compatible OpenAPI endpoint", () => {
|
||||
expect(resolveRNacosOpenApiFallback("http://rnacos.example:10848", "/rnacos")).toEqual({
|
||||
serverAddr: "http://rnacos.example:8848",
|
||||
contextPath: "/nacos",
|
||||
});
|
||||
expect(resolveRNacosOpenApiFallback("https://rnacos.example/gateway", "rnacos/")).toEqual({
|
||||
serverAddr: "https://rnacos.example/gateway",
|
||||
contextPath: "/nacos",
|
||||
});
|
||||
expect(resolveRNacosOpenApiFallback("http://nacos.example:10848", "/nacos")).toBeNull();
|
||||
expect(resolveRNacosOpenApiFallback("http://rnacos.example:10848", "", { allowConsolePortInference: true })).toEqual({
|
||||
serverAddr: "http://rnacos.example:8848",
|
||||
contextPath: "/nacos",
|
||||
});
|
||||
expect(resolveRNacosOpenApiFallback("http://rnacos.example:8848", "/nacos")).toBeNull();
|
||||
});
|
||||
|
||||
it("summarizes config diffs", () => {
|
||||
const diff = summarizeNacosConfigDiff("a\nb", "a\nc\nd");
|
||||
expect(diff.changed).toBe(true);
|
||||
|
|
|
|||
|
|
@ -326,6 +326,8 @@ export const nacosDeleteConfig = forward("nacosDeleteConfig");
|
|||
export const nacosListConfigHistory = forward("nacosListConfigHistory");
|
||||
export const nacosGetConfigHistory = forward("nacosGetConfigHistory");
|
||||
export const nacosRollbackConfig = forward("nacosRollbackConfig");
|
||||
export const nacosGetRNacosConsoleCaptcha = forward("nacosGetRNacosConsoleCaptcha");
|
||||
export const nacosLoginRNacosConsole = forward("nacosLoginRNacosConsole");
|
||||
export const nacosListServices = forward("nacosListServices");
|
||||
export const nacosListInstances = forward("nacosListInstances");
|
||||
export const nacosUpdateInstance = forward("nacosUpdateInstance");
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ import type {
|
|||
NacosConfigRollbackRequest,
|
||||
NacosConfigUpsert,
|
||||
NacosConnectionInfo,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
|
|
@ -2101,6 +2102,14 @@ export async function nacosRollbackConfig(connectionId: string, req: NacosConfig
|
|||
return post("/api/nacos/configs/history/rollback", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetRNacosConsoleCaptcha(connectionId: string): Promise<NacosRNacosConsoleCaptcha> {
|
||||
return post("/api/nacos/rnacos-console/captcha", { connectionId });
|
||||
}
|
||||
|
||||
export async function nacosLoginRNacosConsole(connectionId: string, captcha?: string): Promise<void> {
|
||||
return post("/api/nacos/rnacos-console/login", { connectionId, captcha });
|
||||
}
|
||||
|
||||
export async function nacosListServices(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceList> {
|
||||
return post("/api/nacos/services/list", { connectionId, query });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
NacosConfigRollbackRequest,
|
||||
NacosConfigUpsert,
|
||||
NacosConnectionInfo,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
|
|
@ -66,6 +67,14 @@ export async function nacosRollbackConfig(connectionId: string, req: NacosConfig
|
|||
return invoke("nacos_rollback_config", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetRNacosConsoleCaptcha(connectionId: string): Promise<NacosRNacosConsoleCaptcha> {
|
||||
return invoke("nacos_get_rnacos_console_captcha", { connectionId });
|
||||
}
|
||||
|
||||
export async function nacosLoginRNacosConsole(connectionId: string, captcha?: string): Promise<void> {
|
||||
return invoke("nacos_login_rnacos_console", { connectionId, captcha });
|
||||
}
|
||||
|
||||
export async function nacosListServices(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceList> {
|
||||
return invoke("nacos_list_services", { connectionId, query });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { NacosConfigHistoryItem, NacosConfigItem, NacosInstanceInfo, NacosRawRequest, NacosServiceInfo } from "@/types/nacos";
|
||||
import type { NacosConfigHistoryItem, NacosConfigItem, NacosImplementation, NacosInstanceInfo, NacosRawRequest, NacosServiceInfo, NacosVersionMode } from "@/types/nacos";
|
||||
import { diffChars, diffLines } from "diff";
|
||||
|
||||
export type NacosRawTemplateKey = "serverState" | "namespaceList" | "configDetail" | "serviceList" | "instanceList";
|
||||
|
|
@ -37,6 +37,103 @@ export const NACOS_RAW_TEMPLATES: NacosRawTemplate[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export interface RNacosOpenApiFallback {
|
||||
serverAddr: string;
|
||||
contextPath: string;
|
||||
}
|
||||
|
||||
export interface RNacosOpenApiFallbackOptions {
|
||||
/** Treat the well-known r-nacos console port as a candidate after the original connection fails. */
|
||||
allowConsolePortInference?: boolean;
|
||||
}
|
||||
|
||||
export interface NacosEndpointNormalization {
|
||||
serverAddr: string;
|
||||
contextPath: string;
|
||||
detectedImplementation?: NacosImplementation;
|
||||
detectedVersion?: Exclude<NacosVersionMode, "auto">;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface NacosEndpointNormalizationOptions {
|
||||
implementation?: NacosImplementation;
|
||||
versionMode?: NacosVersionMode;
|
||||
contextPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a pasted browser/API URL into the persisted origin and API context.
|
||||
* We only strip documented Nacos 3 UI routes; every other prefix remains an
|
||||
* explicit context path so reverse proxies are not silently broken.
|
||||
*/
|
||||
export function normalizeNacosEndpoint(input: string, options: NacosEndpointNormalizationOptions = {}): NacosEndpointNormalization {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(input.trim());
|
||||
} catch {
|
||||
throw new Error("Nacos address must be a valid absolute URL");
|
||||
}
|
||||
if (url.username || url.password) throw new Error("Nacos address must not contain embedded credentials");
|
||||
|
||||
const rawPath = url.pathname.replace(/\/+$/, "");
|
||||
const implementation = options.implementation;
|
||||
const versionMode = options.versionMode || "auto";
|
||||
const warnings: string[] = [];
|
||||
const hasRNacosSuffix = /\/rnacos$/i.test(rawPath);
|
||||
const hasNacosSuffix = /\/nacos$/i.test(rawPath);
|
||||
const hasNacos3UiSuffix = /\/(?:next(?:\/index\.html)?|index\.html)$/i.test(rawPath);
|
||||
const detectedImplementation: NacosImplementation | undefined = implementation || (hasRNacosSuffix || url.port === "10848" ? "rnacos" : "nacos");
|
||||
const detectedVersion: Exclude<NacosVersionMode, "auto"> | undefined = detectedImplementation === "rnacos" ? undefined : versionMode === "auto" ? (hasNacos3UiSuffix ? "v3" : hasNacosSuffix ? "v2" : undefined) : versionMode;
|
||||
let contextPath = rawPath;
|
||||
|
||||
if (detectedImplementation === "rnacos") {
|
||||
if (hasRNacosSuffix || url.port === "10848") warnings.push("This looks like an r-nacos console URL; use the compatible API address as the primary endpoint.");
|
||||
contextPath = hasNacosSuffix ? rawPath : options.contextPath?.trim() || "/nacos";
|
||||
} else if (detectedVersion === "v3" || hasNacos3UiSuffix) {
|
||||
contextPath = rawPath.replace(/\/(?:next(?:\/index\.html)?|index\.html)$/i, "");
|
||||
if (hasNacos3UiSuffix) warnings.push("The Nacos 3 console route was removed from the API context.");
|
||||
} else if (!contextPath) {
|
||||
contextPath = options.contextPath?.trim() || (detectedVersion === "v2" ? "/nacos" : "");
|
||||
}
|
||||
url.pathname = "/";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return {
|
||||
serverAddr: url.toString().replace(/\/$/, ""),
|
||||
contextPath: contextPath ? `/${contextPath.replace(/^\/+|\/+$/g, "")}` : "",
|
||||
detectedImplementation,
|
||||
detectedVersion,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* r-nacos exposes its Nacos-compatible OpenAPI on the service port (8848) at
|
||||
* `/nacos`; `/rnacos` on 10848 is the separate web console and rejects these
|
||||
* OpenAPI POST requests. Port-only detection is deliberately opt-in: 10848
|
||||
* may also be a legitimate user mapping for a normal Nacos OpenAPI, so callers
|
||||
* must only use that weaker signal as a tested fallback candidate.
|
||||
*/
|
||||
export function resolveRNacosOpenApiFallback(serverAddr: string, contextPath: string, options: RNacosOpenApiFallbackOptions = {}): RNacosOpenApiFallback | null {
|
||||
const normalizedContextPath = `/${contextPath.trim().replace(/^\/+|\/+$/g, "")}`.replace(/\/$/, "");
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(serverAddr.trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicitRNacosContext = normalizedContextPath === "/rnacos";
|
||||
const inferredRNacosConsolePort = options.allowConsolePortInference === true && parsed.port === "10848";
|
||||
if (!explicitRNacosContext && !inferredRNacosConsolePort) return null;
|
||||
if (parsed.port === "10848") parsed.port = "8848";
|
||||
|
||||
return {
|
||||
serverAddr: parsed.toString().replace(/\/$/, ""),
|
||||
contextPath: "/nacos",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseNacosRawQuery(text: string): Record<string, string> | undefined {
|
||||
const trimmed = text.trim().replace(/^\?/, "");
|
||||
if (!trimmed) return undefined;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export interface NacosCapabilities {
|
||||
supportsConfigManagement: boolean;
|
||||
supportsConfigHistory?: boolean;
|
||||
historyUnavailableReason?: "historyDisabled" | "consoleUrlMissing" | "consoleCredentialsMissing" | "consoleAuthenticationFailed";
|
||||
supportsServiceManagement: boolean;
|
||||
supportsInstanceUpdate: boolean;
|
||||
supportsRawApi: boolean;
|
||||
|
|
@ -16,6 +17,11 @@ export interface NacosConnectionInfo {
|
|||
raw?: unknown;
|
||||
}
|
||||
|
||||
export interface NacosRNacosConsoleCaptcha {
|
||||
required: boolean;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface NacosNamespaceInfo {
|
||||
namespace: string;
|
||||
namespaceShowName: string;
|
||||
|
|
@ -43,10 +49,20 @@ export interface NacosAuthConfig {
|
|||
password?: string;
|
||||
}
|
||||
|
||||
export type NacosImplementation = "nacos" | "rnacos";
|
||||
export type NacosVersionMode = "auto" | "v2" | "v3";
|
||||
export type NacosRNacosConsoleAuth = { kind: "inherit" } | { kind: "usernamePassword"; username: string; password: string };
|
||||
|
||||
export interface NacosAdminConfig {
|
||||
implementation?: NacosImplementation;
|
||||
versionMode?: NacosVersionMode;
|
||||
serverAddr: string;
|
||||
namespace?: string;
|
||||
contextPath?: string;
|
||||
rnacosConsoleAddr?: string;
|
||||
/** Undefined keeps the legacy behaviour: history is enabled when a console address exists. */
|
||||
rnacosHistoryEnabled?: boolean;
|
||||
rnacosConsoleAuth?: NacosRNacosConsoleAuth;
|
||||
auth?: NacosAuthConfig;
|
||||
tlsSkipVerify?: boolean;
|
||||
pageSize?: number;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ quick-xml = "0.37"
|
|||
base64 = "0.22"
|
||||
jsonwebtoken = "9"
|
||||
aes-gcm = "0.10"
|
||||
aes = "0.8"
|
||||
cbc = "0.1"
|
||||
argon2 = "0.5"
|
||||
sha2 = "0.10"
|
||||
async-trait = "0.1"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use sha2::{Digest, Sha256};
|
|||
use crate::ai::AiConfigItem;
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY, NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType, TransportLayerConfig};
|
||||
use crate::saved_sql::SavedSqlLibrary;
|
||||
|
|
@ -37,6 +37,7 @@ const SECRET_KEYS: &[&str] = &[
|
|||
MQ_AUTH_CLIENT_SECRET_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY,
|
||||
NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
];
|
||||
const SSH_TUNNEL_SECRET_PREFIX: &str = "ssh_tunnels.";
|
||||
const TRANSPORT_LAYER_SECRET_PREFIX: &str = "transport_layers.";
|
||||
|
|
@ -720,16 +721,25 @@ fn push_nacos_external_config_secrets(secrets: &mut Vec<ConnectionSecretSnapshot
|
|||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = config
|
||||
if let Some(auth) = config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|external_config| external_config.get("auth"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
push_json_secret(secrets, &config.id, NACOS_AUTH_PASSWORD_KEY, auth, "password");
|
||||
{
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
push_json_secret(secrets, &config.id, NACOS_AUTH_PASSWORD_KEY, auth, "password");
|
||||
}
|
||||
}
|
||||
if let Some(auth) = config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|external_config| external_config.get("rnacosConsoleAuth"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
{
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
push_json_secret(secrets, &config.id, NACOS_RNACOS_CONSOLE_PASSWORD_KEY, auth, "password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -760,17 +770,25 @@ fn scrub_nacos_auth_secrets(config: &mut ConnectionConfig) {
|
|||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = config
|
||||
if let Some(auth) = config
|
||||
.external_config
|
||||
.as_mut()
|
||||
.and_then(|external_config| external_config.get_mut("auth"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") && auth.contains_key("password")
|
||||
{
|
||||
scrub_json_secret(auth, "password");
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
if let Some(auth) = config
|
||||
.external_config
|
||||
.as_mut()
|
||||
.and_then(|external_config| external_config.get_mut("rnacosConsoleAuth"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2027,7 +2027,63 @@ impl AppState {
|
|||
}
|
||||
|
||||
let (host, port) = self.connection_host_port(connection_id, config).await?;
|
||||
nacos_config.with_server_endpoint(&host, port)
|
||||
let nacos_config = nacos_config.with_server_endpoint(&host, port)?;
|
||||
if nacos_config.rnacos_console_addr.is_empty() {
|
||||
return Ok(nacos_config);
|
||||
}
|
||||
|
||||
let console_url = reqwest::Url::parse(&nacos_config.rnacos_console_addr)
|
||||
.map_err(|error| format!("r-nacos console address is invalid: {error}"))?;
|
||||
let console_host = console_url
|
||||
.host_str()
|
||||
.filter(|host| !host.is_empty())
|
||||
.ok_or_else(|| "r-nacos console address does not include a host".to_string())?;
|
||||
let console_port = console_url
|
||||
.port_or_known_default()
|
||||
.ok_or_else(|| "r-nacos console address does not include a port".to_string())?;
|
||||
let transport_layers = self.resolved_transport_layers(config).await?;
|
||||
if transport_layers.is_empty() {
|
||||
return Ok(nacos_config);
|
||||
}
|
||||
let console_transport_id = rnacos_console_transport_id(connection_id);
|
||||
let local_port = match db::transport_layer_tunnel::start_transport_layers(
|
||||
&console_transport_id,
|
||||
&transport_layers,
|
||||
console_host,
|
||||
console_port,
|
||||
&self.tunnels,
|
||||
&self.proxy_tunnels,
|
||||
&self.http_tunnels,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(port) => port,
|
||||
Err(error) => {
|
||||
db::transport_layer_tunnel::stop_transport_layers(
|
||||
&console_transport_id,
|
||||
transport_layers.len(),
|
||||
&self.tunnels,
|
||||
&self.proxy_tunnels,
|
||||
&self.http_tunnels,
|
||||
)
|
||||
.await;
|
||||
return Err(format!("r-nacos console transport failed: {error}"));
|
||||
}
|
||||
};
|
||||
match nacos_config.with_rnacos_console_endpoint("127.0.0.1", local_port) {
|
||||
Ok(nacos_config) => Ok(nacos_config),
|
||||
Err(error) => {
|
||||
db::transport_layer_tunnel::stop_transport_layers(
|
||||
&console_transport_id,
|
||||
transport_layers.len(),
|
||||
&self.tunnels,
|
||||
&self.proxy_tunnels,
|
||||
&self.http_tunnels,
|
||||
)
|
||||
.await;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_stale_connection_pool(&self, pool_key: &str) -> bool {
|
||||
|
|
@ -2797,6 +2853,14 @@ impl AppState {
|
|||
&self.http_tunnels,
|
||||
)
|
||||
.await;
|
||||
db::transport_layer_tunnel::stop_transport_layers(
|
||||
&rnacos_console_transport_id(connection_id),
|
||||
layer_count,
|
||||
&self.tunnels,
|
||||
&self.proxy_tunnels,
|
||||
&self.http_tunnels,
|
||||
)
|
||||
.await;
|
||||
self.tunnels.stop_tunnel(connection_id).await;
|
||||
self.proxy_tunnels.stop_tunnel(connection_id).await;
|
||||
self.http_tunnels.stop_tunnel(connection_id).await;
|
||||
|
|
@ -3249,6 +3313,10 @@ fn connection_remote_endpoint(config: &ConnectionConfig) -> (String, u16) {
|
|||
}
|
||||
}
|
||||
|
||||
fn rnacos_console_transport_id(connection_id: &str) -> String {
|
||||
format!("{connection_id}:rnacos-console")
|
||||
}
|
||||
|
||||
fn parse_mq_admin_host_port(config: &ConnectionConfig) -> Option<(String, u16)> {
|
||||
let value = config
|
||||
.external_config
|
||||
|
|
@ -5670,6 +5738,7 @@ for line in sys.stdin:
|
|||
config.port = 10840;
|
||||
config.external_config = Some(serde_json::json!({
|
||||
"serverAddr": "http://192.168.2.51:10840",
|
||||
"rnacosConsoleAddr": "http://192.168.2.51:10848",
|
||||
"namespace": "public",
|
||||
"contextPath": "",
|
||||
"auth": { "kind": "none" }
|
||||
|
|
@ -5691,8 +5760,11 @@ for line in sys.stdin:
|
|||
|
||||
assert!(nacos_config.server_addr.starts_with("http://127.0.0.1:"));
|
||||
assert_ne!(nacos_config.server_addr, "http://192.168.2.51:10840");
|
||||
assert!(nacos_config.rnacos_console_addr.starts_with("http://127.0.0.1:"));
|
||||
assert_ne!(nacos_config.rnacos_console_addr, "http://192.168.2.51:10848");
|
||||
assert!(nacos_config.connect_override.is_none());
|
||||
state.proxy_tunnels.stop_tunnel("proxied-nacos:transport:0").await;
|
||||
state.proxy_tunnels.stop_tunnel("proxied-nacos:rnacos-console:transport:0").await;
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub const MQ_TOKEN_SIGNING_SECRET_PREFIX: &str = "mq.token_signing.";
|
|||
pub const MQ_TOKEN_SIGNING_KEY: &str = "mq.token_signing.key";
|
||||
pub const NACOS_AUTH_SECRET_PREFIX: &str = "nacos.auth.";
|
||||
pub const NACOS_AUTH_PASSWORD_KEY: &str = "nacos.auth.password";
|
||||
pub const NACOS_RNACOS_CONSOLE_PASSWORD_KEY: &str = "nacos.auth.rnacos_console_password";
|
||||
|
||||
pub trait ConnectionSecretStore {
|
||||
fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String>;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,42 @@ pub enum NacosAuthConfig {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NacosImplementation {
|
||||
#[default]
|
||||
Nacos,
|
||||
#[serde(rename = "rnacos")]
|
||||
RNacos,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NacosVersionMode {
|
||||
#[default]
|
||||
Auto,
|
||||
V2,
|
||||
V3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum NacosRNacosConsoleAuth {
|
||||
#[default]
|
||||
Inherit,
|
||||
UsernamePassword {
|
||||
username: String,
|
||||
password: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosAdminConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub implementation: Option<NacosImplementation>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version_mode: Option<NacosVersionMode>,
|
||||
pub server_addr: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub display_server_addr: String,
|
||||
|
|
@ -24,6 +57,17 @@ pub struct NacosAdminConfig {
|
|||
pub namespace: String,
|
||||
#[serde(default)]
|
||||
pub context_path: String,
|
||||
/// Optional r-nacos authenticated-console address. This is separate from
|
||||
/// the OpenAPI server address because r-nacos exposes console-only APIs
|
||||
/// (including config history) on its console service, normally port 10848.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub rnacos_console_addr: String,
|
||||
/// `None` preserves legacy records where supplying a console address
|
||||
/// implicitly enabled configuration history.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rnacos_history_enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub rnacos_console_auth: NacosRNacosConsoleAuth,
|
||||
#[serde(default)]
|
||||
pub auth: NacosAuthConfig,
|
||||
#[serde(default)]
|
||||
|
|
@ -53,10 +97,15 @@ impl NacosAdminConfig {
|
|||
} else {
|
||||
let scheme = if cfg.ssl { "https" } else { "http" };
|
||||
NacosAdminConfig {
|
||||
implementation: None,
|
||||
version_mode: None,
|
||||
server_addr: format!("{scheme}://{}:{}", cfg.host.trim(), cfg.port),
|
||||
display_server_addr: String::new(),
|
||||
namespace: cfg.database.clone().unwrap_or_default(),
|
||||
context_path: String::new(),
|
||||
rnacos_console_addr: String::new(),
|
||||
rnacos_history_enabled: None,
|
||||
rnacos_console_auth: NacosRNacosConsoleAuth::Inherit,
|
||||
auth: if cfg.username.trim().is_empty() {
|
||||
NacosAuthConfig::None
|
||||
} else {
|
||||
|
|
@ -71,16 +120,29 @@ impl NacosAdminConfig {
|
|||
}
|
||||
|
||||
pub fn validate(mut self) -> Result<Self, String> {
|
||||
self.server_addr = self.server_addr.trim().trim_end_matches('/').to_string();
|
||||
self.server_addr = normalize_endpoint_url(&self.server_addr, "Nacos server address")?;
|
||||
if self.server_addr.is_empty() {
|
||||
return Err("Nacos server address is empty".to_string());
|
||||
}
|
||||
if self.display_server_addr.trim().is_empty() {
|
||||
self.display_server_addr = self.server_addr.clone();
|
||||
} else {
|
||||
self.display_server_addr = self.display_server_addr.trim().trim_end_matches('/').to_string();
|
||||
self.display_server_addr = normalize_endpoint_url(&self.display_server_addr, "Nacos display address")?;
|
||||
}
|
||||
self.context_path = normalize_context_path(&self.context_path);
|
||||
self.rnacos_console_addr = if self.rnacos_console_addr.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
normalize_endpoint_url(&self.rnacos_console_addr, "r-nacos console address")?
|
||||
};
|
||||
if !self.rnacos_console_addr.is_empty() {
|
||||
// Normalization above validates the URL and rejects userinfo.
|
||||
}
|
||||
if let NacosRNacosConsoleAuth::UsernamePassword { username, .. } = &self.rnacos_console_auth {
|
||||
if username.trim().is_empty() {
|
||||
return Err("r-nacos console username is empty".to_string());
|
||||
}
|
||||
}
|
||||
if self.page_size == 0 {
|
||||
self.page_size = default_page_size();
|
||||
}
|
||||
|
|
@ -102,6 +164,55 @@ impl NacosAdminConfig {
|
|||
self.connect_override = None;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_rnacos_console_endpoint(mut self, host: &str, port: u16) -> Result<Self, String> {
|
||||
let mut url = reqwest::Url::parse(&self.rnacos_console_addr)
|
||||
.map_err(|e| format!("r-nacos console address is invalid: {e}"))?;
|
||||
url.set_host(Some(host)).map_err(|_| format!("r-nacos console address host is invalid: {host}"))?;
|
||||
url.set_port(Some(port)).map_err(|_| format!("r-nacos console address port is invalid: {port}"))?;
|
||||
self.rnacos_console_addr = url.to_string().trim_end_matches('/').to_string();
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn rnacos_history_enabled(&self) -> bool {
|
||||
self.rnacos_history_enabled.unwrap_or(!self.rnacos_console_addr.is_empty())
|
||||
}
|
||||
|
||||
pub fn effective_rnacos_console_credentials(&self) -> Result<(&str, &str), String> {
|
||||
match &self.rnacos_console_auth {
|
||||
NacosRNacosConsoleAuth::Inherit => match &self.auth {
|
||||
NacosAuthConfig::UsernamePassword { username, password } if !username.trim().is_empty() => {
|
||||
Ok((username, password))
|
||||
}
|
||||
_ => Err("r-nacos console credentials are unavailable".to_string()),
|
||||
},
|
||||
NacosRNacosConsoleAuth::UsernamePassword { username, password } => {
|
||||
if username.trim().is_empty() {
|
||||
return Err("r-nacos console username is empty".to_string());
|
||||
}
|
||||
Ok((username, password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_effective_rnacos_console_credentials(&self) -> bool {
|
||||
match &self.rnacos_console_auth {
|
||||
NacosRNacosConsoleAuth::Inherit => {
|
||||
matches!(&self.auth, NacosAuthConfig::UsernamePassword { username, .. } if !username.trim().is_empty())
|
||||
}
|
||||
NacosRNacosConsoleAuth::UsernamePassword { username, .. } => !username.trim().is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_endpoint_url(value: &str, label: &str) -> Result<String, String> {
|
||||
let mut url = reqwest::Url::parse(value.trim()).map_err(|e| format!("{label} is invalid: {e}"))?;
|
||||
if !url.username().is_empty() || url.password().is_some() {
|
||||
return Err(format!("{label} must not contain embedded credentials"));
|
||||
}
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Ok(url.to_string().trim_end_matches('/').to_string())
|
||||
}
|
||||
|
||||
pub fn normalize_context_path(path: &str) -> String {
|
||||
|
|
@ -192,6 +303,38 @@ mod tests {
|
|||
assert_eq!(parsed.namespace, "public");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_rnacos_console_address() {
|
||||
let cfg = connection_with_external(serde_json::json!({
|
||||
"serverAddr": "http://127.0.0.1:8848",
|
||||
"rnacosConsoleAddr": " http://127.0.0.1:10848/ ",
|
||||
}));
|
||||
|
||||
let parsed = NacosAdminConfig::from_connection(&cfg).unwrap();
|
||||
assert_eq!(parsed.rnacos_console_addr, "http://127.0.0.1:10848");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_optional_profile_fields_and_rejects_endpoint_userinfo() {
|
||||
let parsed = NacosAdminConfig::from_connection(&connection_with_external(serde_json::json!({
|
||||
"implementation": "rnacos",
|
||||
"versionMode": "auto",
|
||||
"serverAddr": "http://127.0.0.1:8848",
|
||||
"rnacosConsoleAddr": "http://127.0.0.1:10848/rnacos/",
|
||||
"rnacosHistoryEnabled": true,
|
||||
"rnacosConsoleAuth": { "kind": "usernamePassword", "username": "console", "password": "secret" }
|
||||
})))
|
||||
.unwrap();
|
||||
assert!(parsed.rnacos_history_enabled());
|
||||
assert_eq!(parsed.effective_rnacos_console_credentials().unwrap().0, "console");
|
||||
|
||||
let err = NacosAdminConfig::from_connection(&connection_with_external(serde_json::json!({
|
||||
"serverAddr": "http://user:secret@127.0.0.1:8848"
|
||||
})))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("must not contain embedded credentials"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_external_context_path_defaults_to_root() {
|
||||
let cfg = connection_with_external(serde_json::json!({
|
||||
|
|
@ -230,4 +373,18 @@ mod tests {
|
|||
assert_eq!(parsed.context_path, "/console");
|
||||
assert!(parsed.connect_override.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_rnacos_console_endpoint_rewrites_only_host_and_port() {
|
||||
let cfg = connection_with_external(serde_json::json!({
|
||||
"serverAddr": "https://192.168.2.51:8848",
|
||||
"rnacosConsoleAddr": "https://192.168.2.51:10848/gateway",
|
||||
"auth": { "kind": "none" }
|
||||
}));
|
||||
|
||||
let parsed =
|
||||
NacosAdminConfig::from_connection(&cfg).unwrap().with_rnacos_console_endpoint("127.0.0.1", 49153).unwrap();
|
||||
|
||||
assert_eq!(parsed.rnacos_console_addr, "https://127.0.0.1:49153/gateway");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,28 +14,56 @@ pub mod types;
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use crate::models::connection::ConnectionConfig;
|
||||
use crate::nacos::config::NacosAdminConfig;
|
||||
use crate::nacos::http::NacosOpenApiAdmin;
|
||||
use crate::nacos::http::{
|
||||
new_rnacos_console_session, NacosOpenApiAdmin, RNacosConsoleSessionHandle, RNACOS_CONSOLE_SESSION_CACHE_SECS,
|
||||
};
|
||||
use crate::nacos::port::NacosAdmin;
|
||||
|
||||
pub use crate::nacos::config::{NacosAdminConfig as NacosConfig, NacosAuthConfig};
|
||||
pub use crate::nacos::types::*;
|
||||
|
||||
type NacosAdminEntry = (NacosAdminConfig, Arc<dyn NacosAdmin>);
|
||||
/// Values that bind a console token to one r-nacos login context. Deliberately
|
||||
/// exclude unrelated Nacos settings such as namespace and page size, so those
|
||||
/// edits do not force users through another CAPTCHA challenge.
|
||||
///
|
||||
/// This is also the cache key rather than the DBX connection ID: one r-nacos
|
||||
/// console session authorizes every configuration in the same instance, even
|
||||
/// when the UI reaches it through a different connection context.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
struct RNacosConsoleSessionScope {
|
||||
address: String,
|
||||
username: String,
|
||||
password_fingerprint: [u8; 32],
|
||||
tls_skip_verify: bool,
|
||||
}
|
||||
|
||||
struct RNacosConsoleSessionEntry {
|
||||
session: RNacosConsoleSessionHandle,
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NacosAdminRegistry {
|
||||
instances: RwLock<HashMap<String, NacosAdminEntry>>,
|
||||
build_locks: RwLock<HashMap<String, Arc<Mutex<()>>>>,
|
||||
rnacos_console_sessions: RwLock<HashMap<RNacosConsoleSessionScope, RNacosConsoleSessionEntry>>,
|
||||
}
|
||||
|
||||
impl NacosAdminRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self { instances: RwLock::new(HashMap::new()), build_locks: RwLock::new(HashMap::new()) }
|
||||
Self {
|
||||
instances: RwLock::new(HashMap::new()),
|
||||
build_locks: RwLock::new(HashMap::new()),
|
||||
rnacos_console_sessions: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_or_build(&self, cfg: &ConnectionConfig) -> Result<Arc<dyn NacosAdmin>, String> {
|
||||
|
|
@ -66,7 +94,8 @@ impl NacosAdminRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
let admin = build_admin(cfg.clone())?;
|
||||
let rnacos_console_session = self.rnacos_console_session(&cfg).await;
|
||||
let admin = build_admin(cfg.clone(), rnacos_console_session)?;
|
||||
self.instances.write().await.insert(connection_id.to_string(), (cfg, admin.clone()));
|
||||
Ok(admin)
|
||||
}
|
||||
|
|
@ -77,15 +106,113 @@ impl NacosAdminRegistry {
|
|||
}
|
||||
|
||||
pub async fn build_transient_config(&self, cfg: NacosAdminConfig) -> Result<Arc<dyn NacosAdmin>, String> {
|
||||
build_admin(cfg)
|
||||
build_admin(cfg, new_rnacos_console_session())
|
||||
}
|
||||
|
||||
pub async fn drop_connection(&self, connection_id: &str) {
|
||||
self.instances.write().await.remove(connection_id);
|
||||
self.build_locks.write().await.remove(connection_id);
|
||||
}
|
||||
|
||||
async fn rnacos_console_session(&self, cfg: &NacosAdminConfig) -> RNacosConsoleSessionHandle {
|
||||
let Some(scope) = rnacos_console_session_scope(cfg) else {
|
||||
return new_rnacos_console_session();
|
||||
};
|
||||
let mut sessions = self.rnacos_console_sessions.write().await;
|
||||
let now = Instant::now();
|
||||
sessions.retain(|_, entry| entry.expires_at > now);
|
||||
if let Some(entry) = sessions.get(&scope) {
|
||||
return entry.session.clone();
|
||||
}
|
||||
let session = new_rnacos_console_session();
|
||||
// Bound cache retention even when a user removes every matching DBX
|
||||
// connection. Tokens expire independently inside the session; this
|
||||
// entry is retained for one additional token lifetime to allow reuse.
|
||||
sessions.insert(
|
||||
scope,
|
||||
RNacosConsoleSessionEntry {
|
||||
session: session.clone(),
|
||||
expires_at: now + Duration::from_secs(RNACOS_CONSOLE_SESSION_CACHE_SECS * 2),
|
||||
},
|
||||
);
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
fn build_admin(cfg: NacosAdminConfig) -> Result<Arc<dyn NacosAdmin>, String> {
|
||||
Ok(Arc::new(NacosOpenApiAdmin::new(cfg)?))
|
||||
fn rnacos_console_session_scope(cfg: &NacosAdminConfig) -> Option<RNacosConsoleSessionScope> {
|
||||
if cfg.rnacos_console_addr.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (username, password) = cfg.effective_rnacos_console_credentials().ok()?;
|
||||
Some(RNacosConsoleSessionScope {
|
||||
address: cfg.rnacos_console_addr.clone(),
|
||||
username: username.to_string(),
|
||||
password_fingerprint: Sha256::digest(password.as_bytes()).into(),
|
||||
tls_skip_verify: cfg.tls_skip_verify,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_admin(
|
||||
cfg: NacosAdminConfig,
|
||||
rnacos_console_session: RNacosConsoleSessionHandle,
|
||||
) -> Result<Arc<dyn NacosAdmin>, String> {
|
||||
Ok(Arc::new(NacosOpenApiAdmin::new_with_rnacos_console_session(cfg, rnacos_console_session)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nacos::config::{NacosImplementation, NacosRNacosConsoleAuth};
|
||||
|
||||
fn rnacos_config(username: &str, password: &str) -> NacosAdminConfig {
|
||||
NacosAdminConfig {
|
||||
implementation: Some(NacosImplementation::RNacos),
|
||||
version_mode: None,
|
||||
server_addr: "http://127.0.0.1:8848".to_string(),
|
||||
display_server_addr: "http://127.0.0.1:8848".to_string(),
|
||||
namespace: "public".to_string(),
|
||||
context_path: "/nacos".to_string(),
|
||||
rnacos_console_addr: "http://127.0.0.1:10848".to_string(),
|
||||
rnacos_history_enabled: Some(true),
|
||||
rnacos_console_auth: NacosRNacosConsoleAuth::UsernamePassword {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
},
|
||||
auth: NacosAuthConfig::None,
|
||||
tls_skip_verify: false,
|
||||
page_size: 20,
|
||||
connect_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shares_console_session_across_non_auth_connection_changes() {
|
||||
let registry = NacosAdminRegistry::new();
|
||||
let config = rnacos_config("admin", "admin");
|
||||
let first = registry.rnacos_console_session(&config).await;
|
||||
|
||||
let mut namespace_changed = config.clone();
|
||||
namespace_changed.namespace = "tenant-a".to_string();
|
||||
let second = registry.rnacos_console_session(&namespace_changed).await;
|
||||
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replaces_console_session_when_credentials_change() {
|
||||
let registry = NacosAdminRegistry::new();
|
||||
let first = registry.rnacos_console_session(&rnacos_config("admin", "admin")).await;
|
||||
let second = registry.rnacos_console_session(&rnacos_config("admin", "new-password")).await;
|
||||
|
||||
assert!(!Arc::ptr_eq(&first, &second));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shares_console_session_across_connection_contexts_for_the_same_instance() {
|
||||
let registry = NacosAdminRegistry::new();
|
||||
let first = registry.rnacos_console_session(&rnacos_config("admin", "admin")).await;
|
||||
let second = registry.rnacos_console_session(&rnacos_config("admin", "admin")).await;
|
||||
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ pub trait NacosAdmin: Send + Sync {
|
|||
async fn list_config_history(&self, query: NacosConfigHistoryQuery) -> Result<NacosConfigHistoryList, String>;
|
||||
async fn get_config_history(&self, key: NacosConfigHistoryKey) -> Result<NacosConfigItem, String>;
|
||||
async fn rollback_config(&self, req: NacosConfigRollbackRequest) -> Result<(), String>;
|
||||
async fn get_rnacos_console_captcha(&self) -> Result<NacosRNacosConsoleCaptcha, String>;
|
||||
async fn login_rnacos_console(&self, captcha: Option<String>) -> Result<(), String>;
|
||||
async fn list_services(&self, query: NacosServiceQuery) -> Result<NacosServiceList, String>;
|
||||
async fn list_instances(&self, query: NacosInstanceQuery) -> Result<Vec<NacosInstanceInfo>, String>;
|
||||
async fn update_instance(&self, req: NacosInstanceUpdate) -> Result<(), String>;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ pub async fn nacos_test_connection_core(state: &AppState, conn_id: &str) -> Resu
|
|||
return Err("Connection is not a Nacos admin connection".to_string());
|
||||
}
|
||||
let admin_config = state.nacos_admin_config_for_connection(conn_id, &cfg).await?;
|
||||
let admin = state.nacos_registry.build_transient_config(admin_config).await?;
|
||||
// Keep this probe on the connection's shared adapter so an r-nacos console
|
||||
// session verified for configuration history can also expose its version.
|
||||
let admin = state.nacos_registry.get_or_build_config(conn_id, admin_config).await?;
|
||||
admin.test_connection().await
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +97,23 @@ pub async fn nacos_rollback_config_core(
|
|||
admin.rollback_config(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_get_rnacos_console_captcha_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
) -> Result<NacosRNacosConsoleCaptcha, String> {
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
admin.get_rnacos_console_captcha().await
|
||||
}
|
||||
|
||||
pub async fn nacos_login_rnacos_console_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
captcha: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
admin.login_rnacos_console(captcha).await
|
||||
}
|
||||
|
||||
pub async fn nacos_list_services_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct NacosCapabilities {
|
||||
pub supports_config_management: bool,
|
||||
pub supports_config_history: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub history_unavailable_reason: Option<String>,
|
||||
pub supports_service_management: bool,
|
||||
pub supports_instance_update: bool,
|
||||
pub supports_raw_api: bool,
|
||||
|
|
@ -15,6 +17,7 @@ impl Default for NacosCapabilities {
|
|||
Self {
|
||||
supports_config_management: true,
|
||||
supports_config_history: true,
|
||||
history_unavailable_reason: None,
|
||||
supports_service_management: true,
|
||||
supports_instance_update: true,
|
||||
supports_raw_api: true,
|
||||
|
|
@ -22,6 +25,17 @@ impl Default for NacosCapabilities {
|
|||
}
|
||||
}
|
||||
|
||||
/// A short-lived challenge returned by the authenticated r-nacos console.
|
||||
/// The corresponding server-side CAPTCHA token stays in the adapter process;
|
||||
/// only the image is returned to the desktop client.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosRNacosConsoleCaptcha {
|
||||
pub required: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosConnectionInfo {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::ai::{AiChatMessage, AiConfig, AiConfigItem, AiConversation, AiProvide
|
|||
use crate::connection_secrets::{
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_SECRET_PREFIX,
|
||||
MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, MQ_TOKEN_SIGNING_SECRET_PREFIX, NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_AUTH_SECRET_PREFIX,
|
||||
NACOS_AUTH_SECRET_PREFIX, NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
};
|
||||
use crate::db::sqlite::{connect_path_create_if_missing, SqliteHandle};
|
||||
use crate::history::{
|
||||
|
|
@ -659,11 +659,15 @@ fn scrub_nacos_auth_secrets(config: &mut ConnectionConfig) {
|
|||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
if let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) {
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
if let Some(auth) = nacos_console_auth_object_mut(config.external_config.as_mut()) {
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2320,14 +2324,21 @@ impl Storage {
|
|||
if config.db_type != DatabaseType::Nacos {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) != Some("usernamePassword") {
|
||||
return Ok(false);
|
||||
let mut rewritten = false;
|
||||
if let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) {
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
rewritten |=
|
||||
hydrate_mq_json_secret(self, connection_id, NACOS_AUTH_PASSWORD_KEY, auth, "password").await?;
|
||||
}
|
||||
}
|
||||
|
||||
hydrate_mq_json_secret(self, connection_id, NACOS_AUTH_PASSWORD_KEY, auth, "password").await
|
||||
if let Some(auth) = nacos_console_auth_object_mut(config.external_config.as_mut()) {
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
rewritten |=
|
||||
hydrate_mq_json_secret(self, connection_id, NACOS_RNACOS_CONSOLE_PASSWORD_KEY, auth, "password")
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(rewritten)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3280,39 +3291,37 @@ fn persist_nacos_auth_secrets_in_tx(tx: &rusqlite::Transaction<'_>, config: &Con
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(auth) = nacos_auth_object(config.external_config.as_ref()) else {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
replace_nacos_auth_secret_in_tx(tx, &config.id, NACOS_AUTH_PASSWORD_KEY, auth, "password")?;
|
||||
let primary_auth = nacos_auth_object(config.external_config.as_ref())
|
||||
.filter(|auth| auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword"));
|
||||
let primary = primary_auth
|
||||
.and_then(|auth| auth.get("password").and_then(serde_json::Value::as_str))
|
||||
.filter(|secret| !secret.is_empty());
|
||||
let console_auth = nacos_console_auth_object(config.external_config.as_ref())
|
||||
.filter(|auth| auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword"));
|
||||
let console = console_auth
|
||||
.and_then(|auth| auth.get("password").and_then(serde_json::Value::as_str))
|
||||
.filter(|secret| !secret.is_empty());
|
||||
let existing_primary = if primary.is_none() && primary_auth.is_some() {
|
||||
get_secret_in_tx(tx, &config.id, NACOS_AUTH_PASSWORD_KEY)?
|
||||
} else {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
None
|
||||
};
|
||||
let existing_console = if console.is_none() && console_auth.is_some() {
|
||||
get_secret_in_tx(tx, &config.id, NACOS_RNACOS_CONSOLE_PASSWORD_KEY)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
if let Some(secret) = primary.or(existing_primary.as_deref()) {
|
||||
persist_secret_in_tx(tx, &config.id, NACOS_AUTH_PASSWORD_KEY, secret)?;
|
||||
}
|
||||
if let Some(secret) = console.or(existing_console.as_deref()) {
|
||||
persist_secret_in_tx(tx, &config.id, NACOS_RNACOS_CONSOLE_PASSWORD_KEY, secret)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_nacos_auth_secret_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
key: &str,
|
||||
auth: &serde_json::Map<String, serde_json::Value>,
|
||||
field: &str,
|
||||
) -> Result<(), String> {
|
||||
let current = auth.get(field).and_then(serde_json::Value::as_str).filter(|secret| !secret.is_empty());
|
||||
let existing = if current.is_none() { get_secret_in_tx(tx, connection_id, key)? } else { None };
|
||||
delete_secret_prefix_in_tx(tx, connection_id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
match current {
|
||||
Some(secret) => persist_secret_in_tx(tx, connection_id, key, secret),
|
||||
None => match existing {
|
||||
Some(secret) => persist_secret_in_tx(tx, connection_id, key, &secret),
|
||||
None => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_json_secret_if_present_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
|
|
@ -3384,6 +3393,16 @@ fn nacos_auth_object_mut(
|
|||
value?.get_mut("auth")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn nacos_console_auth_object(value: Option<&serde_json::Value>) -> Option<&serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get("rnacosConsoleAuth")?.as_object()
|
||||
}
|
||||
|
||||
fn nacos_console_auth_object_mut(
|
||||
value: Option<&mut serde_json::Value>,
|
||||
) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get_mut("rnacosConsoleAuth")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn is_api_key_auth_kind(kind: &str) -> bool {
|
||||
matches!(kind, "apiKey" | "api_key" | "apikey")
|
||||
}
|
||||
|
|
@ -3417,6 +3436,7 @@ mod tests {
|
|||
maybe_import_user_data_db, DataDbImportResult, DesktopIconTheme, DesktopSettings, McpGlobalPolicy,
|
||||
McpGlobalPolicyState, Storage, MCP_GLOBAL_POLICY_KEY,
|
||||
};
|
||||
use crate::connection_secrets::NACOS_RNACOS_CONSOLE_PASSWORD_KEY;
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
};
|
||||
|
|
@ -4097,6 +4117,39 @@ mod tests {
|
|||
assert_eq!(nacos_auth_password(&loaded[0]), Some("nacos-secret"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_connections_moves_separate_rnacos_console_password_to_secret_table() {
|
||||
let path = temp_db_path("rnacos-console-auth-secret");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
let mut config = nacos_connection("rnacos", "");
|
||||
config.external_config = Some(serde_json::json!({
|
||||
"implementation": "rnacos",
|
||||
"serverAddr": "http://127.0.0.1:8848",
|
||||
"rnacosConsoleAddr": "http://127.0.0.1:10848/rnacos",
|
||||
"rnacosHistoryEnabled": true,
|
||||
"auth": { "kind": "none" },
|
||||
"rnacosConsoleAuth": { "kind": "usernamePassword", "username": "console", "password": "console-secret" }
|
||||
}));
|
||||
|
||||
storage.save_connections(&[config]).await.unwrap();
|
||||
let raw_json = raw_connection_json(&storage, "rnacos").await;
|
||||
assert!(!raw_json.contains("console-secret"));
|
||||
assert_eq!(
|
||||
storage.get_secret("rnacos", NACOS_RNACOS_CONSOLE_PASSWORD_KEY).await.unwrap().as_deref(),
|
||||
Some("console-secret")
|
||||
);
|
||||
let loaded = storage.load_connections().await.unwrap();
|
||||
assert_eq!(
|
||||
loaded[0]
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("rnacosConsoleAuth"))
|
||||
.and_then(|auth| auth.get("password"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("console-secret")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_connections_migrates_legacy_nacos_auth_password_out_of_config_json() {
|
||||
let path = temp_db_path("nacos-auth-legacy-migration");
|
||||
|
|
|
|||
|
|
@ -461,6 +461,8 @@ async fn main() {
|
|||
.route("/nacos/configs/history/list", post(routes::nacos::list_config_history))
|
||||
.route("/nacos/configs/history/get", post(routes::nacos::get_config_history))
|
||||
.route("/nacos/configs/history/rollback", post(routes::nacos::rollback_config))
|
||||
.route("/nacos/rnacos-console/captcha", post(routes::nacos::get_rnacos_console_captcha))
|
||||
.route("/nacos/rnacos-console/login", post(routes::nacos::login_rnacos_console))
|
||||
.route("/nacos/services/list", post(routes::nacos::list_services))
|
||||
.route("/nacos/instances/list", post(routes::nacos::list_instances))
|
||||
.route("/nacos/instances/update", post(routes::nacos::update_instance))
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ pub(crate) struct ConfigRollbackReq {
|
|||
req: dbx_core::nacos::NacosConfigRollbackRequest,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct RNacosConsoleLoginReq {
|
||||
connection_id: String,
|
||||
#[serde(default)]
|
||||
captcha: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ServiceListReq {
|
||||
|
|
@ -206,6 +214,26 @@ pub async fn rollback_config(
|
|||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn get_rnacos_console_captcha(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ConnReq>,
|
||||
) -> Result<Json<dbx_core::nacos::NacosRNacosConsoleCaptcha>, AppError> {
|
||||
let result = dbx_core::nacos::service::nacos_get_rnacos_console_captcha_core(&state.app, &req.connection_id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn login_rnacos_console(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<RNacosConsoleLoginReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_login_rnacos_console_core(&state.app, &req.connection_id, req.captcha)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn list_services(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ServiceListReq>,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,23 @@ pub async fn nacos_rollback_config(
|
|||
dbx_core::nacos::service::nacos_rollback_config_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_get_rnacos_console_captcha(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<dbx_core::nacos::NacosRNacosConsoleCaptcha, String> {
|
||||
dbx_core::nacos::service::nacos_get_rnacos_console_captcha_core(&state, &connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_login_rnacos_console(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
captcha: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_login_rnacos_console_core(&state, &connection_id, captcha).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_list_services(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -1533,6 +1533,8 @@ pub fn run() {
|
|||
commands::nacos_cmd::nacos_list_config_history,
|
||||
commands::nacos_cmd::nacos_get_config_history,
|
||||
commands::nacos_cmd::nacos_rollback_config,
|
||||
commands::nacos_cmd::nacos_get_rnacos_console_captcha,
|
||||
commands::nacos_cmd::nacos_login_rnacos_console,
|
||||
commands::nacos_cmd::nacos_list_services,
|
||||
commands::nacos_cmd::nacos_list_instances,
|
||||
commands::nacos_cmd::nacos_update_instance,
|
||||
|
|
|
|||
Loading…
Reference in New Issue