From 11b2504ce59277189869bc8ff6c774f8355ced7f Mon Sep 17 00:00:00 2001 From: lwt <215586800@qq.com> Date: Thu, 18 Jun 2026 14:41:15 +0800 Subject: [PATCH] Show SVN repository auth hints --- assets/js/svn-admin.js | 15 +++++++---- server/svn-service.js | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/assets/js/svn-admin.js b/assets/js/svn-admin.js index 419fe22..85201fe 100644 --- a/assets/js/svn-admin.js +++ b/assets/js/svn-admin.js @@ -152,7 +152,7 @@ function renderRepos() { els.repoList.innerHTML = visibleRepos.map((repo) => ``).join("") || `

${repositories.length ? "没有匹配的仓库。" : "暂无 SVN 仓库,可以先点击“接入旧仓库”。"}

`; const repo = selectedRepo(); @@ -163,7 +163,7 @@ function renderRepos() { els.selectedRepoUrl.textContent = compactPath(repo?.url); els.selectedRepoPath.textContent = compactPath(repo?.localPath || repo?.checkoutPath); els.selectedRepoRevision.textContent = repo?.serverRevision ? `r${repo.serverRevision}` : (repo?.lastAction?.at ? formatDate(repo.lastAction.at) : "--"); - els.selectedRepoSource.textContent = repo ? (repo.source === "server" ? "服务器旧仓库" : "手动配置") : "--"; + els.selectedRepoSource.textContent = repo ? (repo.source === "server" ? `服务器旧仓库${repo.serverAuthRequired ? " · 需要 SVN 凭据" : ""}` : "手动配置") : "--"; els.deleteRepo.disabled = !repo; } @@ -172,13 +172,18 @@ function renderServerRepos() { els.serverSummary.textContent = serverRepositories.length ? `发现 ${serverRepositories.length} 个,已接入 ${configuredCount} 个` : "未发现旧仓库"; - els.serverRepoList.innerHTML = serverRepositories.map((repo) => ``).join("") || `

没有扫描到服务器 SVN 仓库。

`; + ${repo.configured ? "已接入" : "待接入"}${repo.revision ? ` · r${escapeHtml(repo.revision)}` : ""}
${escapeHtml(authText)}
+ `; + }).join("") || `

没有扫描到服务器 SVN 仓库。

`; } async function loadRepos() { diff --git a/server/svn-service.js b/server/svn-service.js index 4b4bb13..d64465a 100644 --- a/server/svn-service.js +++ b/server/svn-service.js @@ -54,6 +54,11 @@ function limitText(value, maxLength = 4000) { return String(value || "").trim().slice(0, maxLength); } +function limitStringArray(value, maxItems = 30, maxLength = 120) { + if (!Array.isArray(value)) return []; + return value.map((item) => limitText(item, maxLength)).filter(Boolean).slice(0, maxItems); +} + function isRemoteTarget(value) { return REMOTE_TARGET_PATTERN.test(String(value || "").trim()); } @@ -138,6 +143,41 @@ async function readTextIfExists(filePath) { } } +function parseSimpleConfig(text) { + const result = {}; + for (const line of String(text || "").split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[")) continue; + const index = trimmed.indexOf("="); + if (index === -1) continue; + result[trimmed.slice(0, index).trim()] = trimmed.slice(index + 1).trim(); + } + return result; +} + +function parsePasswdUsers(text) { + const users = []; + let inUsers = false; + for (const line of String(text || "").split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue; + if (/^\[users\]$/i.test(trimmed)) { + inUsers = true; + continue; + } + if (/^\[.+\]$/.test(trimmed)) { + inUsers = false; + continue; + } + if (!inUsers) continue; + const index = trimmed.indexOf("="); + if (index === -1) continue; + const username = trimmed.slice(0, index).trim(); + if (username) users.push(username); + } + return users; +} + function addAuthArgs(args, repo) { const next = [...args, "--non-interactive"]; if (repo.trustServerCert !== false) { @@ -250,6 +290,9 @@ async function discoveredRepoInfo(options, req, rootPath, repoPath) { const name = relativePath || path.basename(repoPath); const current = await readTextIfExists(path.join(repoPath, "db", "current")); const revision = current.split(/\s+/)[0] || ""; + const svnserveConfig = parseSimpleConfig(await readTextIfExists(path.join(repoPath, "conf", "svnserve.conf"))); + const authUsers = parsePasswdUsers(await readTextIfExists(path.join(repoPath, "conf", svnserveConfig["password-db"] || "passwd"))); + const anonAccess = svnserveConfig["anon-access"] || ""; return { id: slugify(name), name, @@ -261,6 +304,11 @@ async function discoveredRepoInfo(options, req, rootPath, repoPath) { uuid: await readTextIfExists(path.join(repoPath, "db", "uuid")), fsType: await readTextIfExists(path.join(repoPath, "db", "fs-type")), format: await readTextIfExists(path.join(repoPath, "format")), + anonAccess, + authAccess: svnserveConfig["auth-access"] || "", + realm: svnserveConfig.realm || "", + authRequired: anonAccess.toLowerCase() === "none", + authUsers: authUsers.slice(0, 30), }; } @@ -323,6 +371,11 @@ function repoFromDiscovered(discovered, existing = {}) { serverRelativePath: discovered.relativePath, serverRevision: discovered.revision, serverUuid: discovered.uuid, + serverAnonAccess: discovered.anonAccess, + serverAuthAccess: discovered.authAccess, + serverRealm: discovered.realm, + serverAuthRequired: discovered.authRequired, + serverAuthUsers: discovered.authUsers, description: existing.description || `从服务器 SVN 服务接入:${discovered.repositoryPath}`, }, existing); } @@ -398,6 +451,11 @@ function normalizeRepoPatch(body, existing = {}) { serverRelativePath: limitText(body.serverRelativePath || existing.serverRelativePath, 1000), serverRevision: limitText(body.serverRevision || existing.serverRevision, 80), serverUuid: limitText(body.serverUuid || existing.serverUuid, 120), + serverAnonAccess: limitText(body.serverAnonAccess || existing.serverAnonAccess, 80), + serverAuthAccess: limitText(body.serverAuthAccess || existing.serverAuthAccess, 80), + serverRealm: limitText(body.serverRealm || existing.serverRealm, 200), + serverAuthRequired: parseBoolean(body.serverAuthRequired, existing.serverAuthRequired === true), + serverAuthUsers: limitStringArray(body.serverAuthUsers || existing.serverAuthUsers), updatedAt: new Date().toISOString(), createdAt: existing.createdAt || new Date().toISOString(), };