fix: prevent Gitea token corruption from masked config values, add real connection test

This commit is contained in:
2026-05-14 16:36:41 +08:00
parent 69473320b3
commit 7e735cdf72
5 changed files with 79 additions and 17 deletions
+13 -8
View File
@@ -104,15 +104,20 @@ class ConfigManager:
logger.info(f"已创建默认配置文件: {self.config_file}")
def save_config(self) -> None:
"""保存配置到文件(API 密钥不写入文件)"""
# 保存前临时清空 API 密钥,避免写入文件
"""保存配置到文件(敏感字段不写入文件)"""
# 保存前临时清空敏感字段,避免写入文件
saved_keys = {}
for option in ('api_key', 'secret_key'):
sensitive_fields = [
('API', 'api_key'),
('API', 'secret_key'),
('Gitea', 'token'),
]
for section, option in sensitive_fields:
try:
saved_keys[option] = self.config.get('API', option, fallback='')
saved_keys[(section, option)] = self.config.get(section, option, fallback='')
except Exception:
saved_keys[option] = ''
self.config.set('API', option, '')
saved_keys[(section, option)] = ''
self.config.set(section, option, '')
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
@@ -120,9 +125,9 @@ class ConfigManager:
logger.info(f"配置已保存到: {self.config_file}")
finally:
# 恢复内存中的值(即使写入失败也恢复)
for option, val in saved_keys.items():
for (section, option), val in saved_keys.items():
if val:
self.config.set('API', option, val)
self.config.set(section, option, val)
def get(self, section: str, option: str, fallback: Any = None) -> Any:
"""获取配置值"""
+1 -1
View File
@@ -37,5 +37,5 @@ item_data = 商品资料.xlsx
base_url = https://gitea.94kan.cn
owner = houhuan
repo = yixuan-sync-data
token =
token = 50b61e43a141d606ae2529cd1755bc666d800e08
+17 -1
View File
@@ -64,6 +64,11 @@ async def get_config(
return result
def _is_masked(key: str, value: str) -> bool:
"""Check if a value looks like a masked sensitive field (contains asterisks)."""
return any(s in key.lower() for s in _SENSITIVE_KEYS) and '*' in value
@router.put("")
async def update_config(
body: ConfigUpdate,
@@ -72,6 +77,9 @@ async def update_config(
if body.section not in _ALLOWED_SECTIONS:
raise HTTPException(403, f"不允许修改配置节: {body.section}")
if _is_masked(body.key, body.value):
raise HTTPException(400, "敏感字段不能直接提交掩码值,请先清除输入框再输入真实值")
cfg = _get_config()
try:
cfg.update(body.section, body.key, body.value)
@@ -88,11 +96,19 @@ async def bulk_update_config(
):
cfg = _get_config()
updated = []
skipped = []
for item in body.updates:
if item.section not in _ALLOWED_SECTIONS:
continue
# Skip masked sensitive values to prevent destroying real credentials
if _is_masked(item.key, item.value):
skipped.append(f"[{item.section}] {item.key}")
continue
cfg.update(item.section, item.key, item.value)
updated.append(f"[{item.section}] {item.key}")
cfg.save_config()
return {"message": f"已更新 {len(updated)}", "updated": updated}
msg = f"已更新 {len(updated)}"
if skipped:
msg += f",跳过 {len(skipped)} 项掩码值"
return {"message": msg, "updated": updated, "skipped": skipped}
+20 -3
View File
@@ -78,6 +78,7 @@ async def sync_status(
):
try:
from app.config.settings import ConfigManager
import httpx as _httpx
cfg = ConfigManager()
base_url = cfg.get("Gitea", "base_url", fallback="").strip()
owner = cfg.get("Gitea", "owner", fallback="").strip()
@@ -85,6 +86,22 @@ async def sync_status(
token = cfg.get("Gitea", "token", fallback="").strip()
enabled = bool(base_url and owner and repo and token)
repo_url = f"{base_url}/{owner}/{repo}" if enabled else ""
return {"enabled": enabled, "repo_url": repo_url}
except Exception:
return {"enabled": False, "repo_url": ""}
connected = False
error = ""
if enabled:
try:
async with _httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{base_url}/api/v1/repos/{owner}/{repo}",
headers={"Authorization": f"token {token}"},
)
connected = resp.status_code == 200
if not connected:
error = f"Gitea 返回 {resp.status_code}"
except Exception as e:
error = str(e)
return {"enabled": enabled, "connected": connected, "repo_url": repo_url, "error": error}
except Exception as e:
return {"enabled": False, "connected": False, "repo_url": "", "error": str(e)}
+28 -4
View File
@@ -10,16 +10,24 @@
<!-- Enabled state -->
<div v-if="syncStatus.enabled" class="sync-enabled">
<!-- Connection info -->
<div class="connection-card">
<div class="connection-card" :class="{ 'connection-error': !syncStatus.connected }">
<div class="connection-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--success)" stroke-width="1.5">
<svg v-if="syncStatus.connected" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--success)" stroke-width="1.5">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<svg v-else width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
</div>
<div class="connection-info">
<span class="connection-status">已连接</span>
<span class="connection-status" :class="{ 'status-error': !syncStatus.connected }">
{{ syncStatus.connected ? '已连接' : '连接失败' }}
</span>
<span class="connection-url">{{ syncStatus.repo_url }}</span>
<span v-if="syncStatus.error" class="connection-error-msg">{{ syncStatus.error }}</span>
</div>
</div>
@@ -96,7 +104,7 @@ import api from '../api'
const processingStore = useProcessingStore()
const syncing = ref(false)
const syncStatus = ref({ enabled: false, repo_url: '' })
const syncStatus = ref({ enabled: false, connected: false, repo_url: '', error: '' })
const currentTask = computed(() => {
if (processingStore.taskSource === 'sync') return processingStore.currentTask
@@ -214,6 +222,10 @@ onMounted(checkStatus)
color: var(--success);
}
.connection-status.status-error {
color: var(--danger);
}
.connection-url {
display: block;
font-size: 13px;
@@ -222,6 +234,18 @@ onMounted(checkStatus)
margin-top: 2px;
}
.connection-error {
background: rgba(239,68,68,0.05);
border-color: rgba(239,68,68,0.15);
}
.connection-error-msg {
display: block;
font-size: 12px;
color: var(--danger);
margin-top: 4px;
}
/* ── Sync actions ── */
.sync-actions {
display: grid;