98 lines
2.6 KiB
Python
98 lines
2.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from paths import DATA_DIR
|
||
|
|
|
||
|
|
SETTINGS_FILE = DATA_DIR / "settings.json"
|
||
|
|
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
SECRET_FIELDS = {
|
||
|
|
"gitlab_token",
|
||
|
|
"xai_api_key",
|
||
|
|
"pts_password",
|
||
|
|
"teams_password",
|
||
|
|
"compbase_password",
|
||
|
|
}
|
||
|
|
PASSWORD_FIELDS = {"pts_password", "teams_password", "compbase_password"}
|
||
|
|
MASK_PLACEHOLDER = "********"
|
||
|
|
|
||
|
|
|
||
|
|
def _now_iso() -> str:
|
||
|
|
return datetime.now(timezone.utc).isoformat()
|
||
|
|
|
||
|
|
|
||
|
|
def load_overrides() -> dict[str, Any]:
|
||
|
|
if not SETTINGS_FILE.exists():
|
||
|
|
return {}
|
||
|
|
try:
|
||
|
|
payload = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
||
|
|
return payload.get("values", payload) if isinstance(payload, dict) else {}
|
||
|
|
except (json.JSONDecodeError, OSError):
|
||
|
|
return {}
|
||
|
|
|
||
|
|
|
||
|
|
def save_overrides(values: dict[str, Any]) -> None:
|
||
|
|
SETTINGS_FILE.write_text(
|
||
|
|
json.dumps(
|
||
|
|
{"updated_at": _now_iso(), "values": values},
|
||
|
|
ensure_ascii=False,
|
||
|
|
indent=2,
|
||
|
|
),
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def mask_secret(value: str) -> str:
|
||
|
|
if not value:
|
||
|
|
return ""
|
||
|
|
if len(value) <= 10:
|
||
|
|
return MASK_PLACEHOLDER
|
||
|
|
return f"{value[:6]}...{value[-4:]}"
|
||
|
|
|
||
|
|
|
||
|
|
def is_masked_or_empty(value: Any) -> bool:
|
||
|
|
if value is None:
|
||
|
|
return True
|
||
|
|
text = str(value).strip()
|
||
|
|
if not text:
|
||
|
|
return True
|
||
|
|
if text == MASK_PLACEHOLDER:
|
||
|
|
return True
|
||
|
|
if "..." in text and len(text) < 30:
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def merge_secret_updates(
|
||
|
|
current: dict[str, Any],
|
||
|
|
updates: dict[str, Any],
|
||
|
|
existing_secrets: dict[str, str],
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
merged = {**current, **updates}
|
||
|
|
for field in SECRET_FIELDS:
|
||
|
|
if field in updates and is_masked_or_empty(updates[field]):
|
||
|
|
if existing_secrets.get(field):
|
||
|
|
merged[field] = existing_secrets[field]
|
||
|
|
else:
|
||
|
|
merged.pop(field, None)
|
||
|
|
return merged
|
||
|
|
|
||
|
|
|
||
|
|
def public_view(settings: dict[str, Any]) -> dict[str, Any]:
|
||
|
|
public = dict(settings)
|
||
|
|
for field in SECRET_FIELDS:
|
||
|
|
raw = public.get(field, "")
|
||
|
|
public[f"{field}_set"] = bool(raw)
|
||
|
|
public[field] = mask_secret(raw) if raw else ""
|
||
|
|
public["gitlab_board_label_list"] = [
|
||
|
|
part.strip()
|
||
|
|
for part in str(public.get("gitlab_board_labels", "")).split(",")
|
||
|
|
if part.strip()
|
||
|
|
]
|
||
|
|
public["auto_fill_time"] = (
|
||
|
|
f"{int(public.get('auto_fill_hour', 18)):02d}:"
|
||
|
|
f"{int(public.get('auto_fill_minute', 0)):02d}"
|
||
|
|
)
|
||
|
|
return public
|