headless_lms_base/
config.rs

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    /// Attempts to create an ApplicationConfiguration from environment variables.
22    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    /// Attempts to create an AzureChatbotConfiguration from environment variables.
80    /// Returns `Ok(Some(AzureChatbotConfiguration))` if both environment variables are set.
81    /// Returns `Ok(None)` if no environment variables are set for chatbot.
82    /// Returns an error if set environment variables fail to parse.
83    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    /// Attempts to create an AzureSearchConfiguration from environment variables.
112    /// Returns `Ok(Some(AzureSearchConfiguration))` if all related environment variables are set.
113    /// Returns `Ok(None)` if no environment variables are set for search and vectorizer.
114    /// Returns an error if set environment variables fail to parse.
115    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    /// Attempts to create an AzureBlobStorageConfiguration from environment variables.
162    /// Returns `Ok(Some(AzureBlobStorageConfiguration))` if both environment variables are set.
163    /// Returns `Ok(None)` if no environment variables are set for blob storage.
164    /// Returns an error if set environment variables fail to parse.
165    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    /// Attempts to create an AzureConfiguration by calling the individual try_from_env functions.
196    /// Returns `Ok(Some(AzureConfiguration))` if any of the configurations are set.
197    /// Returns `Ok(None)` if no relevant environment variables are set.
198    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    /// Creates an AzureConfiguration with empty and mock values to be used in testing and dev
215    /// environments when Azure access is not needed. Enables the azure chatbot functionality to be
216    /// mocked with the api_endpoint from our application.
217    /// Returns `Ok(Some(AzureConfiguration))`
218    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    /// Secret key for HMAC-SHA-256 hashing of OAuth tokens (access tokens, refresh tokens, auth codes).
250    pub oauth_token_hmac_key: String,
251    /// Secret key for signing DPoP nonces (HMAC).
252    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    /// Attempts to create an OAuthServerConfiguration.
266    /// Return `Ok(Some(OAuthConfiguration))` if all configurations are set.
267    /// Return `Err` if any is not set.
268    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}