Skip to main content

haste_server/
config.rs

1use derivative::Derivative;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Deserialize, Serialize)]
6#[serde(default)]
7pub struct ServerConfig {
8    pub allow_artifact_mutations: bool,
9    /// Used for JWT signing/verification.
10    pub certification_dir: PathBuf,
11    /// Main root where the FHIR server is hosted.
12    pub api_uri: String,
13    /// Where to redirect for the hardcoded admin app.
14    pub admin_app_redirect_uri: String,
15
16    pub fhir: FHIRConfig,
17    pub repo: RepoConfig,
18    pub search: SearchConfig,
19    pub email: Option<EmailConfig>,
20    pub rate_limits: RateLimitsConfig,
21    pub max_request_body_size: usize,
22    pub monitoring: MonitoringConfig,
23    pub security: SecurityConfig,
24    pub operations: OperationsConfig,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
28#[serde(default)]
29pub struct SecurityConfig {
30    pub publicize_fhir_metadata: bool,
31    pub mfa: MFAConfig,
32    pub encryption: SecretProviderConfig,
33    pub aes_key: Option<String>,
34    pub certification_key: Option<String>,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
38#[serde(default)]
39pub struct MFAConfig {
40    pub max_credentials_per_user: usize,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44#[serde(default)]
45pub struct OperationsConfig {
46    /// Number of dedicated OS threads in the pool that executes custom
47    /// (tenant-authored) FHIR operation scripts in isolated Deno/V8
48    /// sandboxes. Each thread handles one script invocation at a time, so
49    /// this is a hard ceiling on how many custom operations can run
50    /// concurrently. Defaults to roughly half the host's available
51    /// parallelism.
52    pub deno_pool_threads: usize,
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize)]
56#[serde(default)]
57#[derive(Default)]
58pub struct MonitoringConfig {
59    pub audit_enabled: bool,
60    pub ip_source: IpSource,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum SecretProviderConfig {
66    Environment { prefix: Option<String> },
67    GCP { project_id: String },
68    AWS { region: String },
69}
70
71#[derive(Debug, Clone, Deserialize, Serialize)]
72#[serde(default)]
73pub struct FHIRConfig {
74    /// Max delete limit for type-delete and system-delete operations.
75    pub delete_limit: u64,
76}
77
78// Repo backend where the FHIR server stores its data/resources.
79#[derive(Debug, Clone, Deserialize, Serialize)]
80#[serde(tag = "backend", rename_all = "snake_case")]
81pub enum RepoConfig {
82    Postgres(PostgresConfig),
83}
84
85#[derive(Derivative, Clone, Deserialize, Serialize)]
86#[derivative(Debug)]
87pub struct PostgresConfig {
88    #[derivative(Debug = "ignore")]
89    pub database_url: String,
90    pub max_connections: u32,
91}
92
93// Search backend where the FHIR server stores its search indices.
94#[derive(Debug, Clone, Deserialize, Serialize)]
95#[serde(tag = "backend", rename_all = "snake_case")]
96pub enum SearchConfig {
97    Elasticsearch(ElasticsearchConfig),
98    Postgres(PostgresSearchConfig),
99}
100
101#[derive(Derivative, Clone, Deserialize, Serialize)]
102#[derivative(Debug)]
103pub struct PostgresSearchConfig {
104    #[derivative(Debug = "ignore")]
105    pub database_url: String,
106    pub max_connections: u32,
107}
108
109#[derive(Derivative, Clone, Deserialize, Serialize)]
110#[derivative(Debug)]
111pub struct ElasticsearchConfig {
112    pub url: String,
113    #[derivative(Debug = "ignore")]
114    pub username: String,
115    #[derivative(Debug = "ignore")]
116    pub password: String,
117    /// Allows `migrate search` to rebuild the index when a search parameter
118    /// is removed, dropping its column and already-indexed data.
119    /// Elasticsearch mappings are append-only, so dropping a column requires
120    /// reindexing into a fresh index.  By default this is set to false and will
121    /// only log which parameters would be dropped.
122    #[serde(default)]
123    pub prune_removed_search_parameters: bool,
124}
125
126#[derive(Derivative, Clone, Deserialize, Serialize)]
127#[derivative(Debug)]
128#[serde(tag = "backend", rename_all = "snake_case")]
129pub enum EmailConfig {
130    SendGrid {
131        #[derivative(Debug = "ignore")]
132        api_key: String,
133        #[derivative(Debug = "ignore")]
134        from_address: String,
135    },
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
139#[serde(default)]
140pub struct RateLimitsConfig {
141    pub rate_limit_subscription_tiers: Option<[usize; 4]>,
142    pub rate_limit_window_seconds: u64,
143    pub rate_limit_operation_points: u32,
144}
145
146#[derive(Debug, Clone, Deserialize, Serialize, Default)]
147#[serde(rename_all = "snake_case")]
148pub enum IpSource {
149    #[default]
150    ConnectInfo,
151    CfConnectingIp,
152    XRealIp,
153}
154
155impl Default for FHIRConfig {
156    fn default() -> Self {
157        Self { delete_limit: 100 }
158    }
159}
160
161impl Default for ServerConfig {
162    fn default() -> Self {
163        Self {
164            allow_artifact_mutations: false,
165            certification_dir: PathBuf::from("certifications"),
166            api_uri: "http://localhost:3000".into(),
167            admin_app_redirect_uri: "http://*.localhost:3001".into(),
168            fhir: FHIRConfig::default(),
169            repo: RepoConfig::default(),
170            search: SearchConfig::default(),
171            email: None,
172            max_request_body_size: 4 * 1024 * 1024,
173            rate_limits: RateLimitsConfig::default(),
174            monitoring: MonitoringConfig::default(),
175            security: SecurityConfig::default(),
176            operations: OperationsConfig::default(),
177        }
178    }
179}
180impl Default for RepoConfig {
181    fn default() -> Self {
182        RepoConfig::Postgres(PostgresConfig::default())
183    }
184}
185impl Default for PostgresConfig {
186    fn default() -> Self {
187        Self {
188            database_url: "postgresql://postgres:postgres@localhost:5432/haste_health".into(),
189            max_connections: 10,
190        }
191    }
192}
193impl Default for SearchConfig {
194    fn default() -> Self {
195        SearchConfig::Elasticsearch(ElasticsearchConfig::default())
196    }
197}
198impl Default for ElasticsearchConfig {
199    fn default() -> Self {
200        Self {
201            url: "http://localhost:9200".into(),
202            username: "elastic".into(),
203            password: "elastic".into(),
204            prune_removed_search_parameters: false,
205        }
206    }
207}
208
209impl Default for PostgresSearchConfig {
210    fn default() -> Self {
211        Self {
212            database_url: "postgresql://postgres:postgres@localhost:5432/haste_search".into(),
213            max_connections: 10,
214        }
215    }
216}
217
218impl Default for RateLimitsConfig {
219    fn default() -> Self {
220        Self {
221            rate_limit_subscription_tiers: None,
222            rate_limit_window_seconds: 60 * 60 * 24, // 1 day in seconds
223            rate_limit_operation_points: 100,
224        }
225    }
226}
227
228impl Default for SecurityConfig {
229    fn default() -> Self {
230        Self {
231            publicize_fhir_metadata: true,
232            mfa: MFAConfig::default(),
233            encryption: SecretProviderConfig::default(),
234            aes_key: None,
235            certification_key: None,
236        }
237    }
238}
239
240impl Default for MFAConfig {
241    fn default() -> Self {
242        Self {
243            max_credentials_per_user: 1,
244        }
245    }
246}
247
248impl Default for OperationsConfig {
249    fn default() -> Self {
250        let available = std::thread::available_parallelism()
251            .map(std::num::NonZeroUsize::get)
252            .unwrap_or(4);
253
254        Self {
255            deno_pool_threads: (available / 2).max(2),
256        }
257    }
258}
259
260impl Default for SecretProviderConfig {
261    fn default() -> Self {
262        Self::Environment {
263            prefix: Some("HASTE_SECRET_".to_string()),
264        }
265    }
266}