1pub mod cache;
4pub mod document_schema_processor;
5pub mod email_processor;
6pub mod error;
7pub mod file_store;
8pub mod folder_checksum;
9pub mod futures;
10pub mod http;
11pub mod icu4x;
12pub mod ip_to_country;
13pub mod language_tag_to_name;
14pub mod merge_edits;
15pub mod numbers;
16pub mod page_visit_hasher;
17pub mod pagination;
18pub mod prelude;
19pub mod strings;
20pub mod tmc;
21pub mod url_encoding;
22pub mod url_to_oembed_endpoint;
23
24#[macro_use]
25extern crate tracing;
26
27use anyhow::Context;
28use secrecy::{ExposeSecret, SecretBox, SecretString};
29use std::sync::Arc;
30use std::{env, str::FromStr};
31use url::Url;
32
33#[derive(Clone)]
34pub struct ApplicationConfiguration {
35 pub base_url: String,
36 pub test_mode: bool,
37 pub test_chatbot: bool,
38 pub development_uuid_login: bool,
39 pub azure_configuration: Option<AzureConfiguration>,
40 pub tmc_account_creation_origin: Option<String>,
41 pub tmc_admin_access_token: SecretString,
42 pub oauth_server_configuration: OAuthServerConfiguration,
43}
44
45impl ApplicationConfiguration {
46 pub fn try_from_env() -> anyhow::Result<Self> {
48 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
49 let test_mode = env::var("TEST_MODE").is_ok();
50 let development_uuid_login = env::var("DEVELOPMENT_UUID_LOGIN").is_ok();
51 let test_chatbot = test_mode
52 && (env::var("USE_MOCK_AZURE_CONFIGURATION").is_ok_and(|v| v.as_str() != "false")
53 || env::var("AZURE_CHATBOT_API_KEY").is_err());
54
55 let azure_configuration = if test_chatbot {
56 AzureConfiguration::mock_conf()?
57 } else {
58 AzureConfiguration::try_from_env()?
59 };
60
61 let tmc_account_creation_origin = Some(
62 env::var("TMC_ACCOUNT_CREATION_ORIGIN")
63 .context("TMC_ACCOUNT_CREATION_ORIGIN must be defined")?,
64 );
65
66 let tmc_admin_access_token = SecretString::new(
67 std::env::var("TMC_ACCESS_TOKEN")
68 .unwrap_or_else(|_| {
69 if test_mode {
70 "mock-access-token".to_string()
71 } else {
72 panic!("TMC_ACCESS_TOKEN must be defined in production")
73 }
74 })
75 .into(),
76 );
77 let oauth_server_configuration = OAuthServerConfiguration::try_from_env()
78 .context("Failed to load OAuth server configuration")?;
79
80 Ok(Self {
81 base_url,
82 test_mode,
83 test_chatbot,
84 development_uuid_login,
85 azure_configuration,
86 tmc_account_creation_origin,
87 tmc_admin_access_token,
88 oauth_server_configuration,
89 })
90 }
91}
92
93#[derive(Clone, PartialEq)]
94pub struct AzureChatbotConfiguration {
95 pub api_key: String,
96 pub api_endpoint: Url,
97}
98
99impl AzureChatbotConfiguration {
100 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
105 let api_key = env::var("AZURE_CHATBOT_API_KEY").ok();
106 let api_endpoint_str = env::var("AZURE_CHATBOT_API_ENDPOINT").ok();
107
108 if let (Some(api_key), Some(api_endpoint_str)) = (api_key, api_endpoint_str) {
109 let api_endpoint = Url::parse(&api_endpoint_str)
110 .context("Invalid URL in AZURE_CHATBOT_API_ENDPOINT")?;
111 Ok(Some(AzureChatbotConfiguration {
112 api_key,
113 api_endpoint,
114 }))
115 } else {
116 Ok(None)
117 }
118 }
119}
120
121#[derive(Clone, PartialEq)]
122pub struct AzureSearchConfiguration {
123 pub vectorizer_resource_uri: String,
124 pub vectorizer_deployment_id: String,
125 pub vectorizer_api_key: String,
126 pub vectorizer_model_name: String,
127 pub search_endpoint: Url,
128 pub search_api_key: String,
129}
130
131impl AzureSearchConfiguration {
132 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
137 let vectorizer_resource_uri = env::var("AZURE_VECTORIZER_RESOURCE_URI").ok();
138 let vectorizer_deployment_id = env::var("AZURE_VECTORIZER_DEPLOYMENT_ID").ok();
139 let vectorizer_api_key = env::var("AZURE_VECTORIZER_API_KEY").ok();
140 let vectorizer_model_name = env::var("AZURE_VECTORIZER_MODEL_NAME").ok();
141 let search_endpoint_str = env::var("AZURE_SEARCH_ENDPOINT").ok();
142 let search_api_key = env::var("AZURE_SEARCH_API_KEY").ok();
143
144 if let (
145 Some(vectorizer_resource_uri),
146 Some(vectorizer_deployment_id),
147 Some(vectorizer_api_key),
148 Some(vectorizer_model_name),
149 Some(search_endpoint_str),
150 Some(search_api_key),
151 ) = (
152 vectorizer_resource_uri,
153 vectorizer_deployment_id,
154 vectorizer_api_key,
155 vectorizer_model_name,
156 search_endpoint_str,
157 search_api_key,
158 ) {
159 let search_endpoint =
160 Url::parse(&search_endpoint_str).context("Invalid URL in AZURE_SEARCH_ENDPOINT")?;
161 Ok(Some(AzureSearchConfiguration {
162 vectorizer_resource_uri,
163 vectorizer_deployment_id,
164 vectorizer_api_key,
165 vectorizer_model_name,
166 search_endpoint,
167 search_api_key,
168 }))
169 } else {
170 Ok(None)
171 }
172 }
173}
174
175#[derive(Clone, PartialEq)]
176pub struct AzureBlobStorageConfiguration {
177 pub storage_account: String,
178 pub access_key: String,
179}
180
181impl AzureBlobStorageConfiguration {
182 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
186 let storage_account = env::var("AZURE_BLOB_STORAGE_ACCOUNT").ok();
187 let access_key = env::var("AZURE_BLOB_STORAGE_ACCESS_KEY").ok();
188
189 if let (Some(storage_account), Some(access_key)) = (storage_account, access_key) {
190 Ok(Some(AzureBlobStorageConfiguration {
191 storage_account,
192 access_key,
193 }))
194 } else {
195 Ok(None)
196 }
197 }
198
199 pub fn connection_string(&self) -> anyhow::Result<String> {
200 Ok(format!(
201 "DefaultEndpointsProtocol=https;AccountName={};AccountKey={};EndpointSuffix=core.windows.net",
202 self.storage_account, self.access_key
203 ))
204 }
205}
206
207#[derive(Clone, PartialEq)]
208pub struct AzureConfiguration {
209 pub chatbot_config: Option<AzureChatbotConfiguration>,
210 pub search_config: Option<AzureSearchConfiguration>,
211 pub blob_storage_config: Option<AzureBlobStorageConfiguration>,
212}
213
214impl AzureConfiguration {
215 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
219 let chatbot = AzureChatbotConfiguration::try_from_env()?;
220 let search_config = AzureSearchConfiguration::try_from_env()?;
221 let blob_storage_config = AzureBlobStorageConfiguration::try_from_env()?;
222
223 if chatbot.is_some() || search_config.is_some() || blob_storage_config.is_some() {
224 Ok(Some(AzureConfiguration {
225 chatbot_config: chatbot,
226 search_config,
227 blob_storage_config,
228 }))
229 } else {
230 Ok(None)
231 }
232 }
233
234 pub fn mock_conf() -> anyhow::Result<Option<Self>> {
239 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
240 let chatbot_config = Some(AzureChatbotConfiguration {
241 api_key: "".to_string(),
242 api_endpoint: Url::parse(&base_url)?.join("/api/v0/mock-azure/test/")?,
243 });
244 let search_config = Some(AzureSearchConfiguration {
245 vectorizer_resource_uri: "".to_string(),
246 vectorizer_deployment_id: "".to_string(),
247 vectorizer_api_key: "".to_string(),
248 vectorizer_model_name: "".to_string(),
249 search_api_key: "".to_string(),
250 search_endpoint: Url::from_str("https://example.com/does-not-exist/")?,
251 });
252 let blob_storage_config = Some(AzureBlobStorageConfiguration {
253 storage_account: "".to_string(),
254 access_key: "".to_string(),
255 });
256
257 Ok(Some(AzureConfiguration {
258 chatbot_config,
259 search_config,
260 blob_storage_config,
261 }))
262 }
263}
264
265#[derive(Clone)]
266pub struct OAuthServerConfiguration {
267 pub rsa_public_key: String,
268 pub rsa_private_key: String,
269 pub oauth_token_hmac_key: String,
271 pub dpop_nonce_key: Arc<SecretBox<String>>,
273}
274
275impl PartialEq for OAuthServerConfiguration {
276 fn eq(&self, other: &Self) -> bool {
277 self.rsa_public_key == other.rsa_public_key
278 && self.rsa_private_key == other.rsa_private_key
279 && self.oauth_token_hmac_key == other.oauth_token_hmac_key
280 && self.dpop_nonce_key.expose_secret() == other.dpop_nonce_key.expose_secret()
281 }
282}
283
284impl OAuthServerConfiguration {
285 pub fn try_from_env() -> anyhow::Result<Self> {
289 let rsa_public_key =
290 env::var("OAUTH_RSA_PUBLIC_PEM").context("OAUTH_RSA_PUBLIC_KEY must be defined")?;
291 let rsa_private_key =
292 env::var("OAUTH_RSA_PRIVATE_PEM").context("OAUTH_RSA_PRIVATE_KEY must be defined")?;
293 let oauth_token_hmac_key =
294 env::var("OAUTH_TOKEN_HMAC_KEY").context("OAUTH_TOKEN_HMAC_KEY must be defined")?;
295 let dpop_nonce_key = Arc::new(SecretBox::new(Box::new(
296 env::var("OAUTH_DPOP_NONCE_KEY").context("OAUTH_DPOP_NONCE_KEY must be defined")?,
297 )));
298
299 Ok(Self {
300 rsa_public_key,
301 rsa_private_key,
302 oauth_token_hmac_key,
303 dpop_nonce_key,
304 })
305 }
306}