1use anyhow::Context;
2use secrecy::{ExposeSecret, SecretBox, SecretString};
3use std::sync::Arc;
4use std::{env, str::FromStr};
5use url::Url;
6
7pub fn bool_env_false_by_default(key: &str) -> bool {
9 match env::var(key) {
10 Ok(value) => {
11 let normalized = value.trim().to_ascii_lowercase();
12 !matches!(
13 normalized.as_str(),
14 "" | "false" | "0" | "no" | "off" | "disabled"
15 )
16 }
17 Err(_) => false,
18 }
19}
20
21#[derive(Clone)]
22pub struct ApplicationConfiguration {
23 pub base_url: String,
24 pub test_mode: bool,
25 pub test_chatbot: bool,
26 pub development_uuid_login: bool,
27 pub enable_admin_email_verification: bool,
28 pub azure_configuration: Option<AzureConfiguration>,
29 pub tmc_account_creation_origin: Option<String>,
30 pub tmc_admin_access_token: SecretString,
31 pub oauth_server_configuration: OAuthServerConfiguration,
32}
33
34impl ApplicationConfiguration {
35 pub fn try_from_env() -> anyhow::Result<Self> {
37 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
38 let test_mode = bool_env_false_by_default("TEST_MODE");
39 let development_uuid_login = bool_env_false_by_default("DEVELOPMENT_UUID_LOGIN");
40 let enable_admin_email_verification =
41 bool_env_false_by_default("ENABLE_ADMIN_EMAIL_VERIFICATION");
42 let test_chatbot = test_mode
43 && (bool_env_false_by_default("USE_MOCK_AZURE_CONFIGURATION")
44 || env::var("AZURE_CHATBOT_API_KEY").is_err());
45
46 let azure_configuration = if test_chatbot {
47 AzureConfiguration::mock_conf()?
48 } else {
49 AzureConfiguration::try_from_env()?
50 };
51
52 let tmc_account_creation_origin = Some(
53 env::var("TMC_ACCOUNT_CREATION_ORIGIN")
54 .context("TMC_ACCOUNT_CREATION_ORIGIN must be defined")?,
55 );
56
57 let tmc_admin_access_token = SecretString::new(
58 std::env::var("TMC_ACCESS_TOKEN")
59 .unwrap_or_else(|_| {
60 if test_mode {
61 "mock-access-token".to_string()
62 } else {
63 panic!("TMC_ACCESS_TOKEN must be defined in production")
64 }
65 })
66 .into(),
67 );
68 let oauth_server_configuration = OAuthServerConfiguration::try_from_env()
69 .context("Failed to load OAuth server configuration")?;
70
71 Ok(Self {
72 base_url,
73 test_mode,
74 test_chatbot,
75 development_uuid_login,
76 enable_admin_email_verification,
77 azure_configuration,
78 tmc_account_creation_origin,
79 tmc_admin_access_token,
80 oauth_server_configuration,
81 })
82 }
83}
84
85#[derive(Clone)]
86pub struct AzureChatbotConfiguration {
87 pub api_key: SecretString,
88 pub api_endpoint: Url,
89}
90
91impl AzureChatbotConfiguration {
92 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
97 let api_key = env::var("AZURE_CHATBOT_API_KEY").ok();
98 let api_endpoint_str = env::var("AZURE_CHATBOT_API_ENDPOINT").ok();
99
100 if let (Some(api_key), Some(api_endpoint_str)) = (api_key, api_endpoint_str) {
101 let api_endpoint = Url::parse(&api_endpoint_str)
102 .context("Invalid URL in AZURE_CHATBOT_API_ENDPOINT")?;
103 Ok(Some(AzureChatbotConfiguration {
104 api_key: SecretString::new(api_key.into()),
105 api_endpoint,
106 }))
107 } else {
108 Ok(None)
109 }
110 }
111}
112
113#[derive(Clone)]
114pub struct AzureSearchConfiguration {
115 pub vectorizer_resource_uri: String,
116 pub vectorizer_deployment_id: String,
117 pub vectorizer_api_key: SecretString,
118 pub vectorizer_model_name: String,
119 pub search_endpoint: Url,
120 pub search_api_key: SecretString,
121 pub search_connection_id: String,
122}
123
124impl AzureSearchConfiguration {
125 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
130 let vectorizer_resource_uri = env::var("AZURE_VECTORIZER_RESOURCE_URI").ok();
131 let vectorizer_deployment_id = env::var("AZURE_VECTORIZER_DEPLOYMENT_ID").ok();
132 let vectorizer_api_key = env::var("AZURE_VECTORIZER_API_KEY").ok();
133 let vectorizer_model_name = env::var("AZURE_VECTORIZER_MODEL_NAME").ok();
134 let search_endpoint_str = env::var("AZURE_SEARCH_ENDPOINT").ok();
135 let search_api_key = env::var("AZURE_SEARCH_API_KEY").ok();
136 let search_connection_id = env::var("AZURE_SEARCH_CONNECTION_ID").ok();
137
138 if let (
139 Some(vectorizer_resource_uri),
140 Some(vectorizer_deployment_id),
141 Some(vectorizer_api_key),
142 Some(vectorizer_model_name),
143 Some(search_endpoint_str),
144 Some(search_api_key),
145 Some(search_connection_id),
146 ) = (
147 vectorizer_resource_uri,
148 vectorizer_deployment_id,
149 vectorizer_api_key,
150 vectorizer_model_name,
151 search_endpoint_str,
152 search_api_key,
153 search_connection_id,
154 ) {
155 let search_endpoint =
156 Url::parse(&search_endpoint_str).context("Invalid URL in AZURE_SEARCH_ENDPOINT")?;
157 Ok(Some(AzureSearchConfiguration {
158 vectorizer_resource_uri,
159 vectorizer_deployment_id,
160 vectorizer_api_key: SecretString::new(vectorizer_api_key.into()),
161 vectorizer_model_name,
162 search_endpoint,
163 search_api_key: SecretString::new(search_api_key.into()),
164 search_connection_id,
165 }))
166 } else {
167 Ok(None)
168 }
169 }
170}
171
172#[derive(Clone)]
173pub struct AzureBlobStorageConfiguration {
174 pub storage_account: String,
175 pub access_key: SecretString,
176}
177
178impl AzureBlobStorageConfiguration {
179 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
184 let storage_account = env::var("AZURE_BLOB_STORAGE_ACCOUNT").ok();
185 let access_key = env::var("AZURE_BLOB_STORAGE_ACCESS_KEY").ok();
186
187 if let (Some(storage_account), Some(access_key)) = (storage_account, access_key) {
188 Ok(Some(AzureBlobStorageConfiguration {
189 storage_account,
190 access_key: SecretString::new(access_key.into()),
191 }))
192 } else {
193 Ok(None)
194 }
195 }
196
197 pub fn connection_string(&self) -> anyhow::Result<SecretString> {
202 Ok(SecretString::new(
203 format!(
204 "DefaultEndpointsProtocol=https;AccountName={};AccountKey={};EndpointSuffix=core.windows.net",
205 self.storage_account,
206 self.access_key.expose_secret()
207 )
208 .into(),
209 ))
210 }
211}
212
213#[derive(Clone)]
214pub struct AzureConfiguration {
215 pub chatbot_config: Option<AzureChatbotConfiguration>,
216 pub search_config: Option<AzureSearchConfiguration>,
217 pub blob_storage_config: Option<AzureBlobStorageConfiguration>,
218}
219
220impl AzureConfiguration {
221 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
225 let chatbot = AzureChatbotConfiguration::try_from_env()?;
226 let search_config = AzureSearchConfiguration::try_from_env()?;
227 let blob_storage_config = AzureBlobStorageConfiguration::try_from_env()?;
228
229 if chatbot.is_some() || search_config.is_some() || blob_storage_config.is_some() {
230 Ok(Some(AzureConfiguration {
231 chatbot_config: chatbot,
232 search_config,
233 blob_storage_config,
234 }))
235 } else {
236 Ok(None)
237 }
238 }
239
240 pub fn mock_conf() -> anyhow::Result<Option<Self>> {
245 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
246 let chatbot_config = Some(AzureChatbotConfiguration {
247 api_key: SecretString::new(String::new().into()),
248 api_endpoint: Url::parse(&base_url)?.join("/api/v0/mock-azure/test/v1/responses")?,
249 });
250 let search_config = Some(AzureSearchConfiguration {
251 vectorizer_resource_uri: "".to_string(),
252 vectorizer_deployment_id: "".to_string(),
253 vectorizer_api_key: SecretString::new(String::new().into()),
254 vectorizer_model_name: "".to_string(),
255 search_api_key: SecretString::new(String::new().into()),
256 search_endpoint: Url::from_str("https://example.com/does-not-exist/")?,
257 search_connection_id: "".to_string(),
258 });
259 let blob_storage_config = Some(AzureBlobStorageConfiguration {
260 storage_account: "".to_string(),
261 access_key: SecretString::new(String::new().into()),
262 });
263
264 Ok(Some(AzureConfiguration {
265 chatbot_config,
266 search_config,
267 blob_storage_config,
268 }))
269 }
270}
271
272#[derive(Clone)]
273pub struct OAuthServerConfiguration {
274 pub rsa_public_key: String,
275 pub rsa_private_key: SecretString,
278 pub oauth_token_hmac_key: SecretString,
280 pub dpop_nonce_key: Arc<SecretBox<String>>,
282}
283
284impl PartialEq for OAuthServerConfiguration {
285 fn eq(&self, other: &Self) -> bool {
286 self.rsa_public_key == other.rsa_public_key
287 && self.rsa_private_key.expose_secret() == other.rsa_private_key.expose_secret()
288 && self.oauth_token_hmac_key.expose_secret()
289 == other.oauth_token_hmac_key.expose_secret()
290 && self.dpop_nonce_key.expose_secret() == other.dpop_nonce_key.expose_secret()
291 }
292}
293
294impl OAuthServerConfiguration {
295 pub fn try_from_env() -> anyhow::Result<Self> {
299 let rsa_public_key =
300 env::var("OAUTH_RSA_PUBLIC_PEM").context("OAUTH_RSA_PUBLIC_KEY must be defined")?;
301 let rsa_private_key = SecretString::new(
302 env::var("OAUTH_RSA_PRIVATE_PEM")
303 .context("OAUTH_RSA_PRIVATE_KEY must be defined")?
304 .into(),
305 );
306 let oauth_token_hmac_key = SecretString::new(
307 env::var("OAUTH_TOKEN_HMAC_KEY")
308 .context("OAUTH_TOKEN_HMAC_KEY must be defined")?
309 .into(),
310 );
311 let dpop_nonce_key = Arc::new(SecretBox::new(Box::new(
312 env::var("OAUTH_DPOP_NONCE_KEY").context("OAUTH_DPOP_NONCE_KEY must be defined")?,
313 )));
314
315 Ok(Self {
316 rsa_public_key,
317 rsa_private_key,
318 oauth_token_hmac_key,
319 dpop_nonce_key,
320 })
321 }
322}