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 enable_admin_email_verification: bool,
40 pub azure_configuration: Option<AzureConfiguration>,
41 pub tmc_account_creation_origin: Option<String>,
42 pub tmc_admin_access_token: SecretString,
43 pub oauth_server_configuration: OAuthServerConfiguration,
44}
45
46impl ApplicationConfiguration {
47 pub fn try_from_env() -> anyhow::Result<Self> {
49 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
50 let test_mode = env::var("TEST_MODE").is_ok();
51 let development_uuid_login = env::var("DEVELOPMENT_UUID_LOGIN").is_ok();
52 let enable_admin_email_verification = env::var("ENABLE_ADMIN_EMAIL_VERIFICATION")
53 .map(|v| v.parse::<bool>().unwrap_or(false))
54 .unwrap_or(false);
55 let test_chatbot = test_mode
56 && (env::var("USE_MOCK_AZURE_CONFIGURATION").is_ok_and(|v| v.as_str() != "false")
57 || env::var("AZURE_CHATBOT_API_KEY").is_err());
58
59 let azure_configuration = if test_chatbot {
60 AzureConfiguration::mock_conf()?
61 } else {
62 AzureConfiguration::try_from_env()?
63 };
64
65 let tmc_account_creation_origin = Some(
66 env::var("TMC_ACCOUNT_CREATION_ORIGIN")
67 .context("TMC_ACCOUNT_CREATION_ORIGIN must be defined")?,
68 );
69
70 let tmc_admin_access_token = SecretString::new(
71 std::env::var("TMC_ACCESS_TOKEN")
72 .unwrap_or_else(|_| {
73 if test_mode {
74 "mock-access-token".to_string()
75 } else {
76 panic!("TMC_ACCESS_TOKEN must be defined in production")
77 }
78 })
79 .into(),
80 );
81 let oauth_server_configuration = OAuthServerConfiguration::try_from_env()
82 .context("Failed to load OAuth server configuration")?;
83
84 Ok(Self {
85 base_url,
86 test_mode,
87 test_chatbot,
88 development_uuid_login,
89 enable_admin_email_verification,
90 azure_configuration,
91 tmc_account_creation_origin,
92 tmc_admin_access_token,
93 oauth_server_configuration,
94 })
95 }
96}
97
98#[derive(Clone, PartialEq)]
99pub struct AzureChatbotConfiguration {
100 pub api_key: String,
101 pub api_endpoint: Url,
102}
103
104impl AzureChatbotConfiguration {
105 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
110 let api_key = env::var("AZURE_CHATBOT_API_KEY").ok();
111 let api_endpoint_str = env::var("AZURE_CHATBOT_API_ENDPOINT").ok();
112
113 if let (Some(api_key), Some(api_endpoint_str)) = (api_key, api_endpoint_str) {
114 let api_endpoint = Url::parse(&api_endpoint_str)
115 .context("Invalid URL in AZURE_CHATBOT_API_ENDPOINT")?;
116 Ok(Some(AzureChatbotConfiguration {
117 api_key,
118 api_endpoint,
119 }))
120 } else {
121 Ok(None)
122 }
123 }
124}
125
126#[derive(Clone, PartialEq)]
127pub struct AzureSearchConfiguration {
128 pub vectorizer_resource_uri: String,
129 pub vectorizer_deployment_id: String,
130 pub vectorizer_api_key: String,
131 pub vectorizer_model_name: String,
132 pub search_endpoint: Url,
133 pub search_api_key: String,
134}
135
136impl AzureSearchConfiguration {
137 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
142 let vectorizer_resource_uri = env::var("AZURE_VECTORIZER_RESOURCE_URI").ok();
143 let vectorizer_deployment_id = env::var("AZURE_VECTORIZER_DEPLOYMENT_ID").ok();
144 let vectorizer_api_key = env::var("AZURE_VECTORIZER_API_KEY").ok();
145 let vectorizer_model_name = env::var("AZURE_VECTORIZER_MODEL_NAME").ok();
146 let search_endpoint_str = env::var("AZURE_SEARCH_ENDPOINT").ok();
147 let search_api_key = env::var("AZURE_SEARCH_API_KEY").ok();
148
149 if let (
150 Some(vectorizer_resource_uri),
151 Some(vectorizer_deployment_id),
152 Some(vectorizer_api_key),
153 Some(vectorizer_model_name),
154 Some(search_endpoint_str),
155 Some(search_api_key),
156 ) = (
157 vectorizer_resource_uri,
158 vectorizer_deployment_id,
159 vectorizer_api_key,
160 vectorizer_model_name,
161 search_endpoint_str,
162 search_api_key,
163 ) {
164 let search_endpoint =
165 Url::parse(&search_endpoint_str).context("Invalid URL in AZURE_SEARCH_ENDPOINT")?;
166 Ok(Some(AzureSearchConfiguration {
167 vectorizer_resource_uri,
168 vectorizer_deployment_id,
169 vectorizer_api_key,
170 vectorizer_model_name,
171 search_endpoint,
172 search_api_key,
173 }))
174 } else {
175 Ok(None)
176 }
177 }
178}
179
180#[derive(Clone, PartialEq)]
181pub struct AzureBlobStorageConfiguration {
182 pub storage_account: String,
183 pub access_key: String,
184}
185
186impl AzureBlobStorageConfiguration {
187 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
191 let storage_account = env::var("AZURE_BLOB_STORAGE_ACCOUNT").ok();
192 let access_key = env::var("AZURE_BLOB_STORAGE_ACCESS_KEY").ok();
193
194 if let (Some(storage_account), Some(access_key)) = (storage_account, access_key) {
195 Ok(Some(AzureBlobStorageConfiguration {
196 storage_account,
197 access_key,
198 }))
199 } else {
200 Ok(None)
201 }
202 }
203
204 pub fn connection_string(&self) -> anyhow::Result<String> {
205 Ok(format!(
206 "DefaultEndpointsProtocol=https;AccountName={};AccountKey={};EndpointSuffix=core.windows.net",
207 self.storage_account, self.access_key
208 ))
209 }
210}
211
212#[derive(Clone, PartialEq)]
213pub struct AzureConfiguration {
214 pub chatbot_config: Option<AzureChatbotConfiguration>,
215 pub search_config: Option<AzureSearchConfiguration>,
216 pub blob_storage_config: Option<AzureBlobStorageConfiguration>,
217}
218
219impl AzureConfiguration {
220 pub fn try_from_env() -> anyhow::Result<Option<Self>> {
224 let chatbot = AzureChatbotConfiguration::try_from_env()?;
225 let search_config = AzureSearchConfiguration::try_from_env()?;
226 let blob_storage_config = AzureBlobStorageConfiguration::try_from_env()?;
227
228 if chatbot.is_some() || search_config.is_some() || blob_storage_config.is_some() {
229 Ok(Some(AzureConfiguration {
230 chatbot_config: chatbot,
231 search_config,
232 blob_storage_config,
233 }))
234 } else {
235 Ok(None)
236 }
237 }
238
239 pub fn mock_conf() -> anyhow::Result<Option<Self>> {
244 let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
245 let chatbot_config = Some(AzureChatbotConfiguration {
246 api_key: "".to_string(),
247 api_endpoint: Url::parse(&base_url)?.join("/api/v0/mock-azure/test/")?,
248 });
249 let search_config = Some(AzureSearchConfiguration {
250 vectorizer_resource_uri: "".to_string(),
251 vectorizer_deployment_id: "".to_string(),
252 vectorizer_api_key: "".to_string(),
253 vectorizer_model_name: "".to_string(),
254 search_api_key: "".to_string(),
255 search_endpoint: Url::from_str("https://example.com/does-not-exist/")?,
256 });
257 let blob_storage_config = Some(AzureBlobStorageConfiguration {
258 storage_account: "".to_string(),
259 access_key: "".to_string(),
260 });
261
262 Ok(Some(AzureConfiguration {
263 chatbot_config,
264 search_config,
265 blob_storage_config,
266 }))
267 }
268}
269
270#[derive(Clone)]
271pub struct OAuthServerConfiguration {
272 pub rsa_public_key: String,
273 pub rsa_private_key: String,
274 pub oauth_token_hmac_key: String,
276 pub dpop_nonce_key: Arc<SecretBox<String>>,
278}
279
280impl PartialEq for OAuthServerConfiguration {
281 fn eq(&self, other: &Self) -> bool {
282 self.rsa_public_key == other.rsa_public_key
283 && self.rsa_private_key == other.rsa_private_key
284 && self.oauth_token_hmac_key == other.oauth_token_hmac_key
285 && self.dpop_nonce_key.expose_secret() == other.dpop_nonce_key.expose_secret()
286 }
287}
288
289impl OAuthServerConfiguration {
290 pub fn try_from_env() -> anyhow::Result<Self> {
294 let rsa_public_key =
295 env::var("OAUTH_RSA_PUBLIC_PEM").context("OAUTH_RSA_PUBLIC_KEY must be defined")?;
296 let rsa_private_key =
297 env::var("OAUTH_RSA_PRIVATE_PEM").context("OAUTH_RSA_PRIVATE_KEY must be defined")?;
298 let oauth_token_hmac_key =
299 env::var("OAUTH_TOKEN_HMAC_KEY").context("OAUTH_TOKEN_HMAC_KEY must be defined")?;
300 let dpop_nonce_key = Arc::new(SecretBox::new(Box::new(
301 env::var("OAUTH_DPOP_NONCE_KEY").context("OAUTH_DPOP_NONCE_KEY must be defined")?,
302 )));
303
304 Ok(Self {
305 rsa_public_key,
306 rsa_private_key,
307 oauth_token_hmac_key,
308 dpop_nonce_key,
309 })
310 }
311}