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