""" 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}