Files
languard-servers-manager/backend/core/dal/config_repository.py
Tran G. (Revernomad) Khoa 6511353b55 feat: implement full backend + frontend server detail, settings, and create server pages
Backend:
- Complete FastAPI backend with 42+ REST endpoints (auth, servers, config,
  players, bans, missions, mods, games, system)
- Game adapter architecture with Arma 3 as first-class adapter
- WebSocket real-time events for status, metrics, logs, players
- Background thread system (process monitor, metrics, log tail, RCon poller)
- Fernet encryption for sensitive config fields at rest
- JWT auth with admin/viewer roles, bcrypt password hashing
- SQLite with WAL mode, parameterized queries, migration system
- APScheduler cleanup jobs for logs, metrics, events

Frontend:
- Server Detail page with 7 tabs (overview, config, players, bans,
  missions, mods, logs)
- Settings page with password change and admin user management
- Create Server wizard (4-step; known bug: silent validation failure)
- New hooks: useServerDetail, useAuth, useGames
- New components: ServerHeader, ConfigEditor, PlayerTable, BanTable,
  MissionList, ModList, LogViewer, PasswordChange, UserManager
- WebSocket onEvent callback for real-time log accumulation
- 120 unit tests passing (Vitest + React Testing Library)

Docs:
- Added .gitignore, CLAUDE.md, README.md
- Updated FRONTEND.md, ARCHITECTURE.md with current implementation state
- Added .env.example for backend configuration

Known issues:
- Create Server form: "Next" buttons don't validate before advancing,
  causing silent submit failure when fields are invalid
- Config sub-tabs need UX redesign for non-technical users
2026-04-17 11:58:34 +07:00

163 lines
5.8 KiB
Python

"""
Manages the game_configs table.
Handles Fernet encryption/decryption of sensitive fields transparently.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from core.dal.base_repository import BaseRepository
from core.utils.crypto import decrypt, encrypt, is_encrypted
class ConfigRepository(BaseRepository):
def _encrypt_sensitive(
self, config: dict, sensitive_fields: list[str]
) -> dict:
"""Return new dict with sensitive fields encrypted."""
result = dict(config)
for field in sensitive_fields:
if field in result and result[field] and not is_encrypted(str(result[field])):
result[field] = encrypt(str(result[field]))
return result
def _decrypt_sensitive(
self, config: dict, sensitive_fields: list[str]
) -> dict:
"""Return new dict with sensitive fields decrypted."""
result = dict(config)
for field in sensitive_fields:
if field in result and is_encrypted(str(result[field])):
result[field] = decrypt(str(result[field]))
return result
def get_section(
self,
server_id: int,
section: str,
sensitive_fields: list[str] | None = None,
) -> dict | None:
"""Get a config section. Decrypts sensitive fields automatically."""
row = self._fetchone(
"SELECT * FROM game_configs WHERE server_id = :sid AND section = :sec",
{"sid": server_id, "sec": section},
)
if row is None:
return None
config = json.loads(row["config_json"])
if sensitive_fields:
config = self._decrypt_sensitive(config, sensitive_fields)
config["_meta"] = {
"config_version": row["config_version"],
"schema_version": row["schema_version"],
}
return config
def get_all_sections(
self,
server_id: int,
sensitive_fields_by_section: dict[str, list[str]] | None = None,
) -> dict[str, dict]:
"""Get all config sections for a server."""
rows = self._fetchall(
"SELECT * FROM game_configs WHERE server_id = :sid ORDER BY section",
{"sid": server_id},
)
result = {}
for row in rows:
config = json.loads(row["config_json"])
sf = (sensitive_fields_by_section or {}).get(row["section"], [])
if sf:
config = self._decrypt_sensitive(config, sf)
config["_meta"] = {
"config_version": row["config_version"],
"schema_version": row["schema_version"],
}
result[row["section"]] = config
return result
def upsert_section(
self,
server_id: int,
game_type: str,
section: str,
config_data: dict,
schema_version: str,
sensitive_fields: list[str] | None = None,
expected_config_version: int | None = None,
) -> int:
"""
Upsert a config section.
If expected_config_version is provided, checks optimistic lock.
Returns the new config_version.
Raises ValueError on version conflict (caller returns 409).
"""
now = datetime.now(timezone.utc).isoformat()
# Strip _meta before storing
data_to_store = {k: v for k, v in config_data.items() if k != "_meta"}
# Encrypt sensitive fields
if sensitive_fields:
data_to_store = self._encrypt_sensitive(data_to_store, sensitive_fields)
# Check if row exists
existing = self._fetchone(
"SELECT id, config_version FROM game_configs WHERE server_id = :sid AND section = :sec",
{"sid": server_id, "sec": section},
)
if existing is None:
# Insert
self._execute(
"""
INSERT INTO game_configs
(server_id, game_type, section, config_json, config_version, schema_version, updated_at)
VALUES (:sid, :gt, :sec, :json, 1, :sv, :now)
""",
{
"sid": server_id, "gt": game_type, "sec": section,
"json": json.dumps(data_to_store), "sv": schema_version, "now": now,
},
)
return 1
else:
current_version = existing["config_version"]
if expected_config_version is not None and expected_config_version != current_version:
raise ValueError(
f"CONFIG_VERSION_CONFLICT:{current_version}"
)
new_version = current_version + 1
self._execute(
"""
UPDATE game_configs
SET config_json = :json, config_version = :cv,
schema_version = :sv, updated_at = :now
WHERE server_id = :sid AND section = :sec
""",
{
"json": json.dumps(data_to_store),
"cv": new_version,
"sv": schema_version,
"now": now,
"sid": server_id,
"sec": section,
},
)
return new_version
def delete_sections(self, server_id: int) -> None:
self._execute(
"DELETE FROM game_configs WHERE server_id = :sid",
{"sid": server_id},
)
def get_raw_sections(self, server_id: int) -> dict[str, dict]:
"""Get all sections without decryption — for config file generation."""
rows = self._fetchall(
"SELECT section, config_json FROM game_configs WHERE server_id = :sid",
{"sid": server_id},
)
return {row["section"]: json.loads(row["config_json"]) for row in rows}