From 7e735cdf729098de36c040707fdbf94a624420bc Mon Sep 17 00:00:00 2001 From: houhuan Date: Thu, 14 May 2026 16:36:41 +0800 Subject: [PATCH] fix: prevent Gitea token corruption from masked config values, add real connection test --- app/config/settings.py | 21 ++++++++++++-------- config/config.ini | 2 +- web/backend/routers/config_api.py | 18 ++++++++++++++++- web/backend/routers/sync.py | 23 +++++++++++++++++++--- web/frontend/src/views/Sync.vue | 32 +++++++++++++++++++++++++++---- 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index 17fad3b..e71d33e 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -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: """获取配置值""" diff --git a/config/config.ini b/config/config.ini index 34be985..2fb9649 100644 --- a/config/config.ini +++ b/config/config.ini @@ -37,5 +37,5 @@ item_data = 商品资料.xlsx base_url = https://gitea.94kan.cn owner = houhuan repo = yixuan-sync-data -token = +token = 50b61e43a141d606ae2529cd1755bc666d800e08 diff --git a/web/backend/routers/config_api.py b/web/backend/routers/config_api.py index 2aeecdb..cea01dd 100644 --- a/web/backend/routers/config_api.py +++ b/web/backend/routers/config_api.py @@ -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} diff --git a/web/backend/routers/sync.py b/web/backend/routers/sync.py index 4b5c449..13e027f 100644 --- a/web/backend/routers/sync.py +++ b/web/backend/routers/sync.py @@ -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)} diff --git a/web/frontend/src/views/Sync.vue b/web/frontend/src/views/Sync.vue index 074f493..dcc1903 100644 --- a/web/frontend/src/views/Sync.vue +++ b/web/frontend/src/views/Sync.vue @@ -10,16 +10,24 @@
-
+
- + + + + + +
- 已连接 + + {{ syncStatus.connected ? '已连接' : '连接失败' }} + {{ syncStatus.repo_url }} + {{ syncStatus.error }}
@@ -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;