fix: prevent Gitea token corruption from masked config values, add real connection test
This commit is contained in:
+13
-8
@@ -104,15 +104,20 @@ class ConfigManager:
|
|||||||
logger.info(f"已创建默认配置文件: {self.config_file}")
|
logger.info(f"已创建默认配置文件: {self.config_file}")
|
||||||
|
|
||||||
def save_config(self) -> None:
|
def save_config(self) -> None:
|
||||||
"""保存配置到文件(API 密钥不写入文件)"""
|
"""保存配置到文件(敏感字段不写入文件)"""
|
||||||
# 保存前临时清空 API 密钥,避免写入文件
|
# 保存前临时清空敏感字段,避免写入文件
|
||||||
saved_keys = {}
|
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:
|
try:
|
||||||
saved_keys[option] = self.config.get('API', option, fallback='')
|
saved_keys[(section, option)] = self.config.get(section, option, fallback='')
|
||||||
except Exception:
|
except Exception:
|
||||||
saved_keys[option] = ''
|
saved_keys[(section, option)] = ''
|
||||||
self.config.set('API', option, '')
|
self.config.set(section, option, '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
@@ -120,9 +125,9 @@ class ConfigManager:
|
|||||||
logger.info(f"配置已保存到: {self.config_file}")
|
logger.info(f"配置已保存到: {self.config_file}")
|
||||||
finally:
|
finally:
|
||||||
# 恢复内存中的值(即使写入失败也恢复)
|
# 恢复内存中的值(即使写入失败也恢复)
|
||||||
for option, val in saved_keys.items():
|
for (section, option), val in saved_keys.items():
|
||||||
if val:
|
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:
|
def get(self, section: str, option: str, fallback: Any = None) -> Any:
|
||||||
"""获取配置值"""
|
"""获取配置值"""
|
||||||
|
|||||||
+1
-1
@@ -37,5 +37,5 @@ item_data = 商品资料.xlsx
|
|||||||
base_url = https://gitea.94kan.cn
|
base_url = https://gitea.94kan.cn
|
||||||
owner = houhuan
|
owner = houhuan
|
||||||
repo = yixuan-sync-data
|
repo = yixuan-sync-data
|
||||||
token =
|
token = 50b61e43a141d606ae2529cd1755bc666d800e08
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ async def get_config(
|
|||||||
return result
|
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("")
|
@router.put("")
|
||||||
async def update_config(
|
async def update_config(
|
||||||
body: ConfigUpdate,
|
body: ConfigUpdate,
|
||||||
@@ -72,6 +77,9 @@ async def update_config(
|
|||||||
if body.section not in _ALLOWED_SECTIONS:
|
if body.section not in _ALLOWED_SECTIONS:
|
||||||
raise HTTPException(403, f"不允许修改配置节: {body.section}")
|
raise HTTPException(403, f"不允许修改配置节: {body.section}")
|
||||||
|
|
||||||
|
if _is_masked(body.key, body.value):
|
||||||
|
raise HTTPException(400, "敏感字段不能直接提交掩码值,请先清除输入框再输入真实值")
|
||||||
|
|
||||||
cfg = _get_config()
|
cfg = _get_config()
|
||||||
try:
|
try:
|
||||||
cfg.update(body.section, body.key, body.value)
|
cfg.update(body.section, body.key, body.value)
|
||||||
@@ -88,11 +96,19 @@ async def bulk_update_config(
|
|||||||
):
|
):
|
||||||
cfg = _get_config()
|
cfg = _get_config()
|
||||||
updated = []
|
updated = []
|
||||||
|
skipped = []
|
||||||
for item in body.updates:
|
for item in body.updates:
|
||||||
if item.section not in _ALLOWED_SECTIONS:
|
if item.section not in _ALLOWED_SECTIONS:
|
||||||
continue
|
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)
|
cfg.update(item.section, item.key, item.value)
|
||||||
updated.append(f"[{item.section}] {item.key}")
|
updated.append(f"[{item.section}] {item.key}")
|
||||||
|
|
||||||
cfg.save_config()
|
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}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ async def sync_status(
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
from app.config.settings import ConfigManager
|
from app.config.settings import ConfigManager
|
||||||
|
import httpx as _httpx
|
||||||
cfg = ConfigManager()
|
cfg = ConfigManager()
|
||||||
base_url = cfg.get("Gitea", "base_url", fallback="").strip()
|
base_url = cfg.get("Gitea", "base_url", fallback="").strip()
|
||||||
owner = cfg.get("Gitea", "owner", fallback="").strip()
|
owner = cfg.get("Gitea", "owner", fallback="").strip()
|
||||||
@@ -85,6 +86,22 @@ async def sync_status(
|
|||||||
token = cfg.get("Gitea", "token", fallback="").strip()
|
token = cfg.get("Gitea", "token", fallback="").strip()
|
||||||
enabled = bool(base_url and owner and repo and token)
|
enabled = bool(base_url and owner and repo and token)
|
||||||
repo_url = f"{base_url}/{owner}/{repo}" if enabled else ""
|
repo_url = f"{base_url}/{owner}/{repo}" if enabled else ""
|
||||||
return {"enabled": enabled, "repo_url": repo_url}
|
|
||||||
except Exception:
|
connected = False
|
||||||
return {"enabled": False, "repo_url": ""}
|
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)}
|
||||||
|
|||||||
@@ -10,16 +10,24 @@
|
|||||||
<!-- Enabled state -->
|
<!-- Enabled state -->
|
||||||
<div v-if="syncStatus.enabled" class="sync-enabled">
|
<div v-if="syncStatus.enabled" class="sync-enabled">
|
||||||
<!-- Connection info -->
|
<!-- Connection info -->
|
||||||
<div class="connection-card">
|
<div class="connection-card" :class="{ 'connection-error': !syncStatus.connected }">
|
||||||
<div class="connection-icon">
|
<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"/>
|
<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/>
|
||||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||||
</svg>
|
</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>
|
||||||
<div class="connection-info">
|
<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 class="connection-url">{{ syncStatus.repo_url }}</span>
|
||||||
|
<span v-if="syncStatus.error" class="connection-error-msg">{{ syncStatus.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,7 +104,7 @@ import api from '../api'
|
|||||||
const processingStore = useProcessingStore()
|
const processingStore = useProcessingStore()
|
||||||
|
|
||||||
const syncing = ref(false)
|
const syncing = ref(false)
|
||||||
const syncStatus = ref({ enabled: false, repo_url: '' })
|
const syncStatus = ref({ enabled: false, connected: false, repo_url: '', error: '' })
|
||||||
|
|
||||||
const currentTask = computed(() => {
|
const currentTask = computed(() => {
|
||||||
if (processingStore.taskSource === 'sync') return processingStore.currentTask
|
if (processingStore.taskSource === 'sync') return processingStore.currentTask
|
||||||
@@ -214,6 +222,10 @@ onMounted(checkStatus)
|
|||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-status.status-error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
.connection-url {
|
.connection-url {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -222,6 +234,18 @@ onMounted(checkStatus)
|
|||||||
margin-top: 2px;
|
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 ── */
|
||||||
.sync-actions {
|
.sync-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user