fraiseql_server/server_config/
methods.rs1use super::ServerConfig;
2
3impl ServerConfig {
4 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, String> {
10 let content = std::fs::read_to_string(path.as_ref())
11 .map_err(|e| format!("Cannot read config file: {e}"))?;
12 toml::from_str(&content).map_err(|e| format!("Invalid TOML config: {e}"))
13 }
14
15 #[must_use]
21 pub fn is_production_mode() -> bool {
22 let env = std::env::var("FRAISEQL_ENV")
23 .unwrap_or_else(|_| "production".to_string())
24 .to_lowercase();
25 env != "development" && env != "dev"
26 }
27
28 pub fn validate(&self) -> Result<(), String> {
41 if self.metrics_enabled {
42 match &self.metrics_token {
43 None => {
44 return Err("metrics_enabled is true but metrics_token is not set. \
45 Set FRAISEQL_METRICS_TOKEN or metrics_token in config."
46 .to_string());
47 },
48 Some(token) if token.len() < 16 => {
49 return Err(
50 "metrics_token must be at least 16 characters for security.".to_string()
51 );
52 },
53 Some(_) => {},
54 }
55 }
56
57 if self.admin_api_enabled {
59 match &self.admin_token {
60 None => {
61 return Err("admin_api_enabled is true but admin_token is not set. \
62 Set FRAISEQL_ADMIN_TOKEN or admin_token in config."
63 .to_string());
64 },
65 Some(token) if token.len() < 32 => {
66 return Err(
67 "admin_token must be at least 32 characters for security.".to_string()
68 );
69 },
70 Some(_) => {},
71 }
72
73 if let Some(ref ro_token) = self.admin_readonly_token {
75 if ro_token.len() < 32 {
76 return Err(
77 "admin_readonly_token must be at least 32 characters for security."
78 .to_string(),
79 );
80 }
81 if Some(ro_token) == self.admin_token.as_ref() {
82 return Err("admin_readonly_token must differ from admin_token.".to_string());
83 }
84 }
85 }
86
87 if let Some(ref auth) = self.auth {
89 auth.validate().map_err(|e| e.to_string())?;
90 }
91
92 if let Some(ref tls) = self.tls {
94 if tls.enabled {
95 if !tls.cert_path.exists() {
96 return Err(format!(
97 "TLS enabled but certificate file not found: {}",
98 tls.cert_path.display()
99 ));
100 }
101 if !tls.key_path.exists() {
102 return Err(format!(
103 "TLS enabled but key file not found: {}",
104 tls.key_path.display()
105 ));
106 }
107
108 if !["1.2", "1.3"].contains(&tls.min_version.as_str()) {
110 return Err("TLS min_version must be '1.2' or '1.3'".to_string());
111 }
112
113 if tls.require_client_cert {
115 if let Some(ref ca_path) = tls.client_ca_path {
116 if !ca_path.exists() {
117 return Err(format!("Client CA file not found: {}", ca_path.display()));
118 }
119 } else {
120 return Err(
121 "require_client_cert is true but client_ca_path is not set".to_string()
122 );
123 }
124 }
125 }
126 }
127
128 if self.pool_max_size == 0 {
130 return Err("pool_max_size must be at least 1".to_string());
131 }
132 if self.pool_min_size > self.pool_max_size {
133 return Err(format!(
134 "pool_min_size ({}) must not exceed pool_max_size ({})",
135 self.pool_min_size, self.pool_max_size
136 ));
137 }
138 if self.pool_timeout_secs == 0 {
139 return Err(
140 "pool_timeout_secs must be > 0. A zero-second timeout would cause every \
141 connection acquisition to fail immediately. Use a positive value (e.g. 30) \
142 or remove the field to use the default (30s)."
143 .to_string(),
144 );
145 }
146
147 if let Some(ref db_tls) = self.database_tls {
149 if ![
151 "disable",
152 "allow",
153 "prefer",
154 "require",
155 "verify-ca",
156 "verify-full",
157 ]
158 .contains(&db_tls.postgres_ssl_mode.as_str())
159 {
160 return Err("Invalid postgres_ssl_mode. Must be one of: \
161 disable, allow, prefer, require, verify-ca, verify-full"
162 .to_string());
163 }
164
165 if let Some(ref ca_path) = db_tls.ca_bundle_path {
167 if !ca_path.exists() {
168 return Err(format!("CA bundle file not found: {}", ca_path.display()));
169 }
170 }
171 }
172
173 if let Some(ref rl) = self.rate_limiting {
175 if rl.rps_per_ip > 0 && rl.rps_per_user > 0 && rl.rps_per_ip > rl.rps_per_user {
176 tracing::warn!(
177 rps_per_ip = rl.rps_per_ip,
178 rps_per_user = rl.rps_per_user,
179 "rps_per_ip exceeds rps_per_user — authenticated users are more \
180 restricted than anonymous IPs"
181 );
182 }
183 }
184
185 if Self::is_production_mode() {
187 if self.playground_enabled {
189 return Err("playground_enabled is true in production mode. \
190 Disable the playground or set FRAISEQL_ENV=development. \
191 The playground exposes sensitive schema information."
192 .to_string());
193 }
194
195 if self.cors_enabled && self.cors_origins.is_empty() {
197 return Err("cors_enabled is true but cors_origins is empty in production mode. \
198 This allows requests from ANY origin, which is a security risk. \
199 Explicitly configure cors_origins with your allowed domains, \
200 or disable CORS and set FRAISEQL_ENV=development to bypass this check."
201 .to_string());
202 }
203 }
204
205 Ok(())
206 }
207
208 #[must_use]
210 pub const fn auth_enabled(&self) -> bool {
211 self.auth.is_some()
212 }
213}