Coverage for src / local_deep_research / web / server_config.py: 100%
57 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1"""Server configuration management for web app startup.
3This module handles server configuration that needs to be available before
4Flask app context is established. It provides a bridge between environment
5variables and database settings.
6"""
8import json
9import os
10from pathlib import Path
11from typing import Dict, Any
13from loguru import logger
15from ..config.paths import get_data_dir
16from ..settings.manager import get_typed_setting_value
19def get_server_config_path() -> Path:
20 """Get the path to the server configuration file."""
21 return Path(get_data_dir()) / "server_config.json"
24def load_server_config() -> Dict[str, Any]:
25 """Load server configuration from file or environment variables.
27 Returns:
28 dict: Server configuration with keys: host, port, debug, use_https
29 """
30 config_path = get_server_config_path()
32 # Try to load from config file first
33 saved_config = {}
34 if config_path.exists():
35 try:
36 with open(config_path, "r") as f:
37 saved_config = json.load(f)
38 logger.debug(f"Loaded server config from {config_path}")
39 except Exception as e:
40 logger.warning(f"Failed to load server config: {e}")
42 # Ensure correct typing and check environment variables.
43 config = {
44 "host": get_typed_setting_value(
45 "web.host", saved_config.get("host"), "text", default="0.0.0.0"
46 ),
47 "port": get_typed_setting_value(
48 "web.port", saved_config.get("port"), "number", default=5000
49 ),
50 "debug": get_typed_setting_value(
51 "app.debug", saved_config.get("debug"), "checkbox", default=False
52 ),
53 "use_https": get_typed_setting_value(
54 "web.use_https",
55 saved_config.get("use_https"),
56 "checkbox",
57 default=True,
58 ),
59 "allow_registrations": get_typed_setting_value(
60 "app.allow_registrations",
61 saved_config.get("allow_registrations"),
62 "checkbox",
63 default=True,
64 ),
65 }
67 # Security: if LDR_APP_ALLOW_REGISTRATIONS is set to an unrecognized
68 # value (e.g. "disabled", "nein", typos like "flase"), default to False
69 # (registrations disabled) rather than True. This "fail closed" approach
70 # prevents accidental open registration when an admin clearly intended to
71 # restrict it but used a non-standard boolean string.
72 _RECOGNIZED_BOOL_VALUES = {
73 "true",
74 "false",
75 "1",
76 "0",
77 "yes",
78 "no",
79 "on",
80 "off",
81 }
82 raw_reg_env = os.getenv("LDR_APP_ALLOW_REGISTRATIONS")
83 if raw_reg_env is not None:
84 normalized = raw_reg_env.lower().strip()
85 if normalized not in _RECOGNIZED_BOOL_VALUES:
86 logger.warning(
87 f"LDR_APP_ALLOW_REGISTRATIONS='{raw_reg_env}' is not a "
88 f"recognized boolean value. Defaulting to FALSE "
89 f"(registrations disabled) for security. Use "
90 f"'true'/'false', '1'/'0', 'yes'/'no', or 'on'/'off'."
91 )
92 config["allow_registrations"] = False
94 config.update(
95 {
96 # Rate limiting settings
97 "rate_limit_default": get_typed_setting_value(
98 "security.rate_limit_default",
99 saved_config.get("rate_limit_default"),
100 "text",
101 default="5000 per hour;50000 per day",
102 ),
103 "rate_limit_login": get_typed_setting_value(
104 "security.rate_limit_login",
105 saved_config.get("rate_limit_login"),
106 "text",
107 default="5 per 15 minutes",
108 ),
109 "rate_limit_registration": get_typed_setting_value(
110 "security.rate_limit_registration",
111 saved_config.get("rate_limit_registration"),
112 "text",
113 default="3 per hour",
114 ),
115 }
116 )
118 return config
121def save_server_config(config: Dict[str, Any]) -> None:
122 """Save server configuration to file.
124 This should be called when web.host or web.port settings are updated
125 through the UI.
127 Args:
128 config: Server configuration dict
129 """
130 config_path = get_server_config_path()
132 try:
133 # Ensure directory exists
134 config_path.parent.mkdir(parents=True, exist_ok=True)
136 with open(config_path, "w") as f:
137 json.dump(config, f, indent=2)
139 logger.info(f"Saved server config to {config_path}")
140 except Exception:
141 logger.exception("Failed to save server config")
144def sync_from_settings(settings_snapshot: Dict[str, Any]) -> None:
145 """Sync server config from settings snapshot.
147 This should be called when settings are updated through the UI.
149 Args:
150 settings_snapshot: Settings snapshot containing web.host and web.port
151 """
152 config = load_server_config()
154 # Update from settings if available
155 if "web.host" in settings_snapshot:
156 config["host"] = settings_snapshot["web.host"]
157 if "web.port" in settings_snapshot:
158 config["port"] = settings_snapshot["web.port"]
159 if "app.debug" in settings_snapshot:
160 config["debug"] = settings_snapshot["app.debug"]
161 if "web.use_https" in settings_snapshot:
162 config["use_https"] = settings_snapshot["web.use_https"]
163 if "app.allow_registrations" in settings_snapshot:
164 config["allow_registrations"] = settings_snapshot[
165 "app.allow_registrations"
166 ]
167 if "security.rate_limit_default" in settings_snapshot:
168 config["rate_limit_default"] = settings_snapshot[
169 "security.rate_limit_default"
170 ]
171 if "security.rate_limit_login" in settings_snapshot:
172 config["rate_limit_login"] = settings_snapshot[
173 "security.rate_limit_login"
174 ]
175 if "security.rate_limit_registration" in settings_snapshot:
176 config["rate_limit_registration"] = settings_snapshot[
177 "security.rate_limit_registration"
178 ]
180 save_server_config(config)