feat: add system settings with model connectivity and encrypted storage
This commit is contained in:
@@ -5,7 +5,6 @@ from dataclasses import dataclass
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.core.admin_secret import read_admin_secret, verify_admin_secret
|
||||
from app.core.config import get_settings
|
||||
from app.core.logging import get_logger
|
||||
from app.core.security import verify_password
|
||||
@@ -13,6 +12,7 @@ from app.models.employee import Employee
|
||||
from app.schemas.auth import AuthUserRead, LoginRequest, LoginResponse
|
||||
from app.services.employee import EmployeeService
|
||||
from app.services.employee_seed import ROLE_DISPLAY_ORDER
|
||||
from app.services.settings import SettingsService
|
||||
|
||||
logger = get_logger("app.services.auth")
|
||||
|
||||
@@ -53,34 +53,25 @@ class AuthService:
|
||||
|
||||
employee_user = self._authenticate_employee(identifier, password)
|
||||
if employee_user is not None:
|
||||
logger.info("Employee login succeeded identifier=%s role_codes=%s", identifier, ",".join(employee_user.role_codes))
|
||||
logger.info(
|
||||
"Employee login succeeded identifier=%s role_codes=%s",
|
||||
identifier,
|
||||
",".join(employee_user.role_codes),
|
||||
)
|
||||
return LoginResponse(user=self._serialize_user(employee_user))
|
||||
|
||||
logger.warning("Login failed identifier=%s", identifier)
|
||||
raise ValueError("账号或密码错误。")
|
||||
|
||||
def _authenticate_admin(self, identifier: str, password: str) -> AuthenticatedUser | None:
|
||||
record = read_admin_secret()
|
||||
record = SettingsService(self.db).verify_admin_login(identifier, password)
|
||||
if record is None:
|
||||
return None
|
||||
|
||||
admin_username = str(record.get("username", "")).strip()
|
||||
admin_email = str(self.settings.admin_email or "").strip()
|
||||
normalized_identifier = identifier.casefold()
|
||||
|
||||
allowed_identifiers = {
|
||||
value.casefold()
|
||||
for value in [admin_username, admin_email]
|
||||
if value
|
||||
}
|
||||
|
||||
if normalized_identifier not in allowed_identifiers:
|
||||
return None
|
||||
|
||||
if not verify_admin_secret(password, record):
|
||||
return None
|
||||
|
||||
admin_username = record.account.strip()
|
||||
admin_email = record.email.strip()
|
||||
display_name = admin_username or admin_email or "系统管理员"
|
||||
|
||||
return AuthenticatedUser(
|
||||
username=admin_username or admin_email,
|
||||
name=display_name,
|
||||
|
||||
216
server/src/app/services/model_connectivity.py
Normal file
216
server/src/app/services/model_connectivity.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from app.schemas.settings import ModelConnectivityTestRead, ModelConnectivityTestRequest
|
||||
|
||||
AZURE_API_VERSION = "2024-10-21"
|
||||
DEFAULT_TIMEOUT_SECONDS = 12
|
||||
|
||||
|
||||
class ConnectivityCheckError(Exception):
|
||||
def __init__(self, message: str, status_code: int | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def probe_model_connectivity(payload: ModelConnectivityTestRequest) -> ModelConnectivityTestRead:
|
||||
checked_at = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
if payload.provider == "Azure OpenAI":
|
||||
status_code = _probe_azure_openai(payload)
|
||||
elif payload.provider == "Ollama":
|
||||
status_code = _probe_ollama(payload)
|
||||
else:
|
||||
status_code = _probe_openai_compatible(payload)
|
||||
|
||||
detail = f"{payload.provider} 已连接,模型 {payload.model} 可正常访问。"
|
||||
return ModelConnectivityTestRead(
|
||||
ok=True,
|
||||
provider=payload.provider,
|
||||
model=payload.model,
|
||||
endpoint=payload.endpoint,
|
||||
capability=payload.capability,
|
||||
detail=detail,
|
||||
status_code=status_code,
|
||||
checked_at=checked_at,
|
||||
)
|
||||
except ConnectivityCheckError as exc:
|
||||
return ModelConnectivityTestRead(
|
||||
ok=False,
|
||||
provider=payload.provider,
|
||||
model=payload.model,
|
||||
endpoint=payload.endpoint,
|
||||
capability=payload.capability,
|
||||
detail=str(exc),
|
||||
status_code=exc.status_code,
|
||||
checked_at=checked_at,
|
||||
)
|
||||
|
||||
|
||||
def _probe_openai_compatible(payload: ModelConnectivityTestRequest) -> int:
|
||||
normalized_endpoint = _normalize_endpoint(payload.endpoint)
|
||||
headers = _build_headers(api_key=payload.api_key, use_bearer=True)
|
||||
|
||||
if payload.capability == "embedding":
|
||||
url = _ensure_path(normalized_endpoint, "embeddings")
|
||||
body = {"model": payload.model, "input": "connectivity test"}
|
||||
else:
|
||||
url = _ensure_path(normalized_endpoint, "chat/completions")
|
||||
body = {
|
||||
"model": payload.model,
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
"max_tokens": 1,
|
||||
}
|
||||
|
||||
status_code, _ = _send_json_request("POST", url, headers=headers, payload=body)
|
||||
return status_code
|
||||
|
||||
|
||||
def _probe_ollama(payload: ModelConnectivityTestRequest) -> int:
|
||||
normalized_endpoint = _normalize_endpoint(payload.endpoint)
|
||||
headers = _build_headers(api_key=payload.api_key, use_bearer=False)
|
||||
|
||||
if payload.capability == "embedding":
|
||||
url = _ensure_path(normalized_endpoint, "api/embed")
|
||||
body = {"model": payload.model, "input": "connectivity test"}
|
||||
else:
|
||||
url = _ensure_path(normalized_endpoint, "api/chat")
|
||||
body = {
|
||||
"model": payload.model,
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
status_code, _ = _send_json_request("POST", url, headers=headers, payload=body)
|
||||
return status_code
|
||||
|
||||
|
||||
def _probe_azure_openai(payload: ModelConnectivityTestRequest) -> int:
|
||||
deployment_base = _build_azure_deployment_base(payload.endpoint, payload.model)
|
||||
headers = _build_headers(api_key=payload.api_key, use_bearer=False, use_api_key=True)
|
||||
|
||||
if payload.capability == "embedding":
|
||||
url = f"{deployment_base}/embeddings?api-version={AZURE_API_VERSION}"
|
||||
body = {"input": "connectivity test"}
|
||||
else:
|
||||
url = f"{deployment_base}/chat/completions?api-version={AZURE_API_VERSION}"
|
||||
body = {
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
"max_tokens": 1,
|
||||
}
|
||||
|
||||
status_code, _ = _send_json_request("POST", url, headers=headers, payload=body)
|
||||
return status_code
|
||||
|
||||
|
||||
def _build_azure_deployment_base(endpoint: str, model: str) -> str:
|
||||
normalized_endpoint = _normalize_endpoint(endpoint)
|
||||
quoted_model = quote(model, safe="")
|
||||
|
||||
if "/openai/deployments/" in normalized_endpoint:
|
||||
return normalized_endpoint
|
||||
|
||||
if "/openai/v1" in normalized_endpoint:
|
||||
resource_root = normalized_endpoint.split("/openai/v1", maxsplit=1)[0]
|
||||
return f"{resource_root}/openai/deployments/{quoted_model}"
|
||||
|
||||
if normalized_endpoint.endswith("/openai"):
|
||||
return f"{normalized_endpoint}/deployments/{quoted_model}"
|
||||
|
||||
return f"{normalized_endpoint}/openai/deployments/{quoted_model}"
|
||||
|
||||
|
||||
def _build_headers(
|
||||
api_key: str | None,
|
||||
*,
|
||||
use_bearer: bool,
|
||||
use_api_key: bool = False,
|
||||
) -> dict[str, str]:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
if api_key:
|
||||
if use_api_key:
|
||||
headers["api-key"] = api_key
|
||||
elif use_bearer:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def _normalize_endpoint(endpoint: str) -> str:
|
||||
normalized = endpoint.strip()
|
||||
if not normalized:
|
||||
raise ConnectivityCheckError("接口地址不能为空。", status_code=HTTPStatus.BAD_REQUEST)
|
||||
return normalized.rstrip("/")
|
||||
|
||||
|
||||
def _ensure_path(endpoint: str, suffix: str) -> str:
|
||||
suffix = suffix.lstrip("/")
|
||||
if endpoint.endswith(suffix):
|
||||
return endpoint
|
||||
return f"{endpoint}/{suffix}"
|
||||
|
||||
|
||||
def _send_json_request(
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str],
|
||||
payload: dict[str, Any],
|
||||
) -> tuple[int, Any]:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
request = Request(url=url, data=data, headers=headers, method=method)
|
||||
|
||||
try:
|
||||
with urlopen(request, timeout=DEFAULT_TIMEOUT_SECONDS) as response:
|
||||
body = response.read().decode("utf-8") if response.length != 0 else ""
|
||||
return response.status, _parse_json_body(body)
|
||||
except HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="ignore")
|
||||
message = _extract_error_message(_parse_json_body(body)) or f"模型接口返回 {exc.code}。"
|
||||
raise ConnectivityCheckError(message, status_code=exc.code) from exc
|
||||
except URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
raise ConnectivityCheckError(f"无法连接到模型接口:{reason}") from exc
|
||||
except TimeoutError as exc:
|
||||
raise ConnectivityCheckError("模型接口连接超时,请检查地址或网络。") from exc
|
||||
|
||||
|
||||
def _parse_json_body(body: str) -> Any:
|
||||
if not body:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return {"message": body}
|
||||
|
||||
|
||||
def _extract_error_message(payload: Any) -> str | None:
|
||||
if payload is None:
|
||||
return None
|
||||
|
||||
if isinstance(payload, dict):
|
||||
if isinstance(payload.get("detail"), str):
|
||||
return payload["detail"]
|
||||
if isinstance(payload.get("message"), str):
|
||||
return payload["message"]
|
||||
error_payload = payload.get("error")
|
||||
if isinstance(error_payload, dict) and isinstance(error_payload.get("message"), str):
|
||||
return error_payload["message"]
|
||||
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
|
||||
return None
|
||||
352
server/src/app/services/settings.py
Normal file
352
server/src/app/services/settings.py
Normal file
@@ -0,0 +1,352 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.admin_secret import read_admin_secret, verify_admin_secret
|
||||
from app.core.config import get_settings
|
||||
from app.core.secret_box import decrypt_secret, encrypt_secret
|
||||
from app.core.security import hash_password, verify_password
|
||||
from app.db.base import Base
|
||||
from app.models.system_setting import SystemSetting
|
||||
from app.models.system_setting_secret import SystemSettingSecret
|
||||
from app.repositories.settings import SETTINGS_ROW_ID, SettingsRepository
|
||||
from app.schemas.settings import SettingsRead, SettingsWrite
|
||||
|
||||
MODEL_SECRET_FIELDS = {
|
||||
"main": "main_api_key_encrypted",
|
||||
"backup": "backup_api_key_encrypted",
|
||||
"vlm": "vlm_api_key_encrypted",
|
||||
"embedding": "embedding_api_key_encrypted",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AdminCredentialRecord:
|
||||
account: str
|
||||
email: str
|
||||
password_hash: str
|
||||
|
||||
|
||||
class SettingsService:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
self.repository = SettingsRepository(db)
|
||||
self.runtime_settings = get_settings()
|
||||
|
||||
def ensure_settings_ready(self) -> tuple[SystemSetting, SystemSettingSecret]:
|
||||
Base.metadata.create_all(bind=self.db.get_bind())
|
||||
|
||||
settings_row = self.repository.get_settings()
|
||||
secrets_row = self.repository.get_secrets()
|
||||
should_commit = False
|
||||
|
||||
if settings_row is None:
|
||||
settings_row = self._build_default_settings()
|
||||
self.db.add(settings_row)
|
||||
should_commit = True
|
||||
|
||||
if secrets_row is None:
|
||||
secrets_row = SystemSettingSecret(id=SETTINGS_ROW_ID)
|
||||
self.db.add(secrets_row)
|
||||
should_commit = True
|
||||
|
||||
if should_commit:
|
||||
self.db.commit()
|
||||
self.db.refresh(settings_row)
|
||||
self.db.refresh(secrets_row)
|
||||
|
||||
return settings_row, secrets_row
|
||||
|
||||
def get_settings_snapshot(self) -> SettingsRead:
|
||||
settings_row, secrets_row = self.ensure_settings_ready()
|
||||
return self._serialize(settings_row, secrets_row)
|
||||
|
||||
def save_settings_snapshot(self, payload: SettingsWrite) -> SettingsRead:
|
||||
settings_row, secrets_row = self.ensure_settings_ready()
|
||||
|
||||
if payload.adminForm.newPassword:
|
||||
if len(payload.adminForm.newPassword) < 5:
|
||||
raise ValueError("管理员密码至少需要 5 位。")
|
||||
if payload.adminForm.newPassword != payload.adminForm.confirmPassword:
|
||||
raise ValueError("两次输入的管理员密码不一致。")
|
||||
secrets_row.admin_password_hash = hash_password(payload.adminForm.newPassword)
|
||||
|
||||
settings_row.company_name = payload.companyForm.companyName
|
||||
settings_row.display_name = payload.companyForm.displayName
|
||||
settings_row.company_code = payload.companyForm.companyCode
|
||||
settings_row.record_number = payload.companyForm.recordNumber
|
||||
settings_row.copyright_text = payload.companyForm.copyright
|
||||
|
||||
settings_row.admin_account = payload.adminForm.adminAccount
|
||||
settings_row.admin_email = payload.adminForm.adminEmail
|
||||
settings_row.session_timeout = payload.adminForm.sessionTimeout
|
||||
settings_row.notice_email = payload.adminForm.noticeEmail
|
||||
settings_row.mfa_enabled = payload.adminForm.mfaEnabled
|
||||
settings_row.strong_password = payload.adminForm.strongPassword
|
||||
settings_row.login_alert_enabled = payload.adminForm.loginAlertEnabled
|
||||
|
||||
settings_row.main_provider = payload.llmForm.mainProvider
|
||||
settings_row.main_model = payload.llmForm.mainModel
|
||||
settings_row.main_endpoint = payload.llmForm.mainEndpoint
|
||||
settings_row.backup_provider = payload.llmForm.backupProvider
|
||||
settings_row.backup_model = payload.llmForm.backupModel
|
||||
settings_row.backup_endpoint = payload.llmForm.backupEndpoint
|
||||
settings_row.vlm_provider = payload.llmForm.vlmProvider
|
||||
settings_row.vlm_model = payload.llmForm.vlmModel
|
||||
settings_row.vlm_endpoint = payload.llmForm.vlmEndpoint
|
||||
settings_row.embedding_provider = payload.llmForm.embeddingProvider
|
||||
settings_row.embedding_model = payload.llmForm.embeddingModel
|
||||
settings_row.embedding_endpoint = payload.llmForm.embeddingEndpoint
|
||||
|
||||
self._replace_secret_if_present(secrets_row, "main_api_key_encrypted", payload.llmForm.mainApiKey)
|
||||
self._replace_secret_if_present(secrets_row, "backup_api_key_encrypted", payload.llmForm.backupApiKey)
|
||||
self._replace_secret_if_present(secrets_row, "vlm_api_key_encrypted", payload.llmForm.vlmApiKey)
|
||||
self._replace_secret_if_present(
|
||||
secrets_row,
|
||||
"embedding_api_key_encrypted",
|
||||
payload.llmForm.embeddingApiKey,
|
||||
)
|
||||
|
||||
settings_row.log_level = payload.logForm.level
|
||||
settings_row.retention_days = payload.logForm.retentionDays
|
||||
settings_row.archive_cycle = payload.logForm.archiveCycle
|
||||
settings_row.log_path = payload.logForm.logPath
|
||||
settings_row.alert_email = payload.logForm.alertEmail
|
||||
settings_row.operation_audit = payload.logForm.operationAudit
|
||||
settings_row.login_audit = payload.logForm.loginAudit
|
||||
settings_row.mask_sensitive = payload.logForm.maskSensitive
|
||||
|
||||
settings_row.smtp_host = payload.mailForm.smtpHost
|
||||
settings_row.smtp_port = payload.mailForm.port
|
||||
settings_row.smtp_encryption = payload.mailForm.encryption
|
||||
settings_row.sender_name = payload.mailForm.senderName
|
||||
settings_row.sender_address = payload.mailForm.senderAddress
|
||||
settings_row.smtp_username = payload.mailForm.username
|
||||
settings_row.alert_enabled = payload.mailForm.alertEnabled
|
||||
settings_row.digest_enabled = payload.mailForm.digestEnabled
|
||||
settings_row.digest_time = payload.mailForm.digestTime
|
||||
settings_row.default_receiver = payload.mailForm.defaultReceiver
|
||||
|
||||
self._replace_secret_if_present(secrets_row, "smtp_password_encrypted", payload.mailForm.password)
|
||||
|
||||
self.db.add(settings_row)
|
||||
self.db.add(secrets_row)
|
||||
self.db.commit()
|
||||
self.db.refresh(settings_row)
|
||||
self.db.refresh(secrets_row)
|
||||
|
||||
return self._serialize(settings_row, secrets_row)
|
||||
|
||||
def load_saved_model_api_key(self, slot: str | None) -> str:
|
||||
if not slot or slot not in MODEL_SECRET_FIELDS:
|
||||
return ""
|
||||
|
||||
_, secrets_row = self.ensure_settings_ready()
|
||||
encrypted_value = getattr(secrets_row, MODEL_SECRET_FIELDS[slot], "")
|
||||
if not encrypted_value:
|
||||
return ""
|
||||
|
||||
return decrypt_secret(encrypted_value)
|
||||
|
||||
def get_admin_credentials(self) -> AdminCredentialRecord | None:
|
||||
settings_row, secrets_row = self.ensure_settings_ready()
|
||||
|
||||
if secrets_row.admin_password_hash:
|
||||
return AdminCredentialRecord(
|
||||
account=settings_row.admin_account,
|
||||
email=settings_row.admin_email,
|
||||
password_hash=secrets_row.admin_password_hash,
|
||||
)
|
||||
|
||||
legacy_record = read_admin_secret()
|
||||
if legacy_record is None:
|
||||
return None
|
||||
|
||||
username = str(legacy_record.get("username", "")).strip()
|
||||
email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
|
||||
password_hash = ""
|
||||
|
||||
# Legacy admin.json uses scrypt fields rather than the app password format.
|
||||
# The auth flow handles this file separately when no DB-backed admin password exists.
|
||||
if username or email:
|
||||
return AdminCredentialRecord(account=username, email=email, password_hash=password_hash)
|
||||
|
||||
return None
|
||||
|
||||
def verify_admin_login(self, identifier: str, password: str) -> AdminCredentialRecord | None:
|
||||
settings_row, secrets_row = self.ensure_settings_ready()
|
||||
normalized_identifier = identifier.casefold()
|
||||
|
||||
if secrets_row.admin_password_hash:
|
||||
allowed_identifiers = {
|
||||
value.casefold()
|
||||
for value in [settings_row.admin_account, settings_row.admin_email]
|
||||
if value
|
||||
}
|
||||
|
||||
if normalized_identifier not in allowed_identifiers:
|
||||
return None
|
||||
|
||||
if not verify_password(password, secrets_row.admin_password_hash):
|
||||
return None
|
||||
|
||||
return AdminCredentialRecord(
|
||||
account=settings_row.admin_account,
|
||||
email=settings_row.admin_email,
|
||||
password_hash=secrets_row.admin_password_hash,
|
||||
)
|
||||
|
||||
legacy_record = read_admin_secret()
|
||||
if legacy_record is None:
|
||||
return None
|
||||
|
||||
admin_username = str(legacy_record.get("username", "")).strip()
|
||||
admin_email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
|
||||
allowed_identifiers = {
|
||||
value.casefold()
|
||||
for value in [admin_username, admin_email]
|
||||
if value
|
||||
}
|
||||
|
||||
if normalized_identifier not in allowed_identifiers:
|
||||
return None
|
||||
|
||||
if not verify_admin_secret(password, legacy_record):
|
||||
return None
|
||||
|
||||
return AdminCredentialRecord(account=admin_username, email=admin_email, password_hash="")
|
||||
|
||||
def _build_default_settings(self) -> SystemSetting:
|
||||
current_year = datetime.now().year
|
||||
company_name = str(self.runtime_settings.company_name or "X-Financial").strip() or "X-Financial"
|
||||
company_code = str(self.runtime_settings.company_code or "XF-001").strip() or "XF-001"
|
||||
admin_email = str(self.runtime_settings.admin_email or "").strip()
|
||||
legacy_admin = read_admin_secret() or {}
|
||||
admin_account = str(legacy_admin.get("username", "")).strip() or "superadmin"
|
||||
|
||||
return SystemSetting(
|
||||
id=SETTINGS_ROW_ID,
|
||||
company_name=company_name,
|
||||
display_name=company_name,
|
||||
company_code=company_code,
|
||||
record_number="",
|
||||
copyright_text=f"Copyright © 2024-{current_year} {company_name}. All Rights Reserved.",
|
||||
admin_account=admin_account,
|
||||
admin_email=admin_email,
|
||||
session_timeout=30,
|
||||
notice_email=admin_email,
|
||||
mfa_enabled=True,
|
||||
strong_password=True,
|
||||
login_alert_enabled=True,
|
||||
main_provider="Codex",
|
||||
main_model="codex-mini-latest",
|
||||
main_endpoint="https://api.openai.com/v1",
|
||||
backup_provider="GLM",
|
||||
backup_model="glm-5.1",
|
||||
backup_endpoint="https://open.bigmodel.cn/api/paas/v4/",
|
||||
vlm_provider="Gemini",
|
||||
vlm_model="gemini-2.5-flash",
|
||||
vlm_endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
embedding_provider="GLM",
|
||||
embedding_model="Embedding-3",
|
||||
embedding_endpoint="https://open.bigmodel.cn/api/paas/v4/",
|
||||
log_level="INFO",
|
||||
retention_days=180,
|
||||
archive_cycle="weekly",
|
||||
log_path="server/logs/app.log",
|
||||
alert_email=admin_email,
|
||||
operation_audit=True,
|
||||
login_audit=True,
|
||||
mask_sensitive=True,
|
||||
smtp_host="smtp.exmail.qq.com",
|
||||
smtp_port=465,
|
||||
smtp_encryption="SSL/TLS",
|
||||
sender_name=company_name,
|
||||
sender_address=admin_email,
|
||||
smtp_username=admin_email,
|
||||
alert_enabled=True,
|
||||
digest_enabled=False,
|
||||
digest_time="09:00",
|
||||
default_receiver=admin_email,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _replace_secret_if_present(secret_row: SystemSettingSecret, field_name: str, value: str) -> None:
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
return
|
||||
|
||||
setattr(secret_row, field_name, encrypt_secret(normalized))
|
||||
|
||||
@staticmethod
|
||||
def _serialize(settings_row: SystemSetting, secrets_row: SystemSettingSecret) -> SettingsRead:
|
||||
return SettingsRead(
|
||||
companyForm={
|
||||
"companyName": settings_row.company_name,
|
||||
"displayName": settings_row.display_name,
|
||||
"companyCode": settings_row.company_code,
|
||||
"recordNumber": settings_row.record_number,
|
||||
"copyright": settings_row.copyright_text,
|
||||
},
|
||||
adminForm={
|
||||
"adminAccount": settings_row.admin_account,
|
||||
"adminEmail": settings_row.admin_email,
|
||||
"newPassword": "",
|
||||
"confirmPassword": "",
|
||||
"sessionTimeout": settings_row.session_timeout,
|
||||
"noticeEmail": settings_row.notice_email,
|
||||
"mfaEnabled": settings_row.mfa_enabled,
|
||||
"strongPassword": settings_row.strong_password,
|
||||
"loginAlertEnabled": settings_row.login_alert_enabled,
|
||||
"adminPasswordConfigured": bool(secrets_row.admin_password_hash),
|
||||
},
|
||||
llmForm={
|
||||
"mainProvider": settings_row.main_provider,
|
||||
"mainModel": settings_row.main_model,
|
||||
"mainEndpoint": settings_row.main_endpoint,
|
||||
"mainApiKey": "",
|
||||
"mainApiKeyConfigured": bool(secrets_row.main_api_key_encrypted),
|
||||
"backupProvider": settings_row.backup_provider,
|
||||
"backupModel": settings_row.backup_model,
|
||||
"backupEndpoint": settings_row.backup_endpoint,
|
||||
"backupApiKey": "",
|
||||
"backupApiKeyConfigured": bool(secrets_row.backup_api_key_encrypted),
|
||||
"vlmProvider": settings_row.vlm_provider,
|
||||
"vlmModel": settings_row.vlm_model,
|
||||
"vlmEndpoint": settings_row.vlm_endpoint,
|
||||
"vlmApiKey": "",
|
||||
"vlmApiKeyConfigured": bool(secrets_row.vlm_api_key_encrypted),
|
||||
"embeddingProvider": settings_row.embedding_provider,
|
||||
"embeddingModel": settings_row.embedding_model,
|
||||
"embeddingEndpoint": settings_row.embedding_endpoint,
|
||||
"embeddingApiKey": "",
|
||||
"embeddingApiKeyConfigured": bool(secrets_row.embedding_api_key_encrypted),
|
||||
},
|
||||
logForm={
|
||||
"level": settings_row.log_level,
|
||||
"retentionDays": settings_row.retention_days,
|
||||
"archiveCycle": settings_row.archive_cycle,
|
||||
"logPath": settings_row.log_path,
|
||||
"alertEmail": settings_row.alert_email,
|
||||
"operationAudit": settings_row.operation_audit,
|
||||
"loginAudit": settings_row.login_audit,
|
||||
"maskSensitive": settings_row.mask_sensitive,
|
||||
},
|
||||
mailForm={
|
||||
"smtpHost": settings_row.smtp_host,
|
||||
"port": settings_row.smtp_port,
|
||||
"encryption": settings_row.smtp_encryption,
|
||||
"senderName": settings_row.sender_name,
|
||||
"senderAddress": settings_row.sender_address,
|
||||
"username": settings_row.smtp_username,
|
||||
"password": "",
|
||||
"passwordConfigured": bool(secrets_row.smtp_password_encrypted),
|
||||
"alertEnabled": settings_row.alert_enabled,
|
||||
"digestEnabled": settings_row.digest_enabled,
|
||||
"digestTime": settings_row.digest_time,
|
||||
"defaultReceiver": settings_row.default_receiver,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user