backend/config/
app.rs

1//! Configuration of the server.
2
3use dotenvy::dotenv;
4use reqwest::Url;
5use secrecy::Secret;
6use serde::Deserialize;
7
8/// Environment variables that are required for the configuration of the server.
9#[derive(Deserialize)]
10struct EnvVars {
11    /// The host the server should be started on.
12    pub bind_address_host: String,
13    /// The port the server should be started on.
14    pub bind_address_port: u16,
15    /// The connection string for the database.
16    pub database_url: String,
17    /// The host of the authentication server.
18    pub auth_host: String,
19    /// The `client_id` the frontend should use to log in its users.
20    pub auth_client_id: String,
21    /// The `client_id` the backend uses to communicate with the auth server.
22    pub auth_admin_client_id: Option<String>,
23    /// The `client_secret` the backend uses to communicate with the auth server.
24    pub auth_admin_client_secret: Option<String>,
25    /// The `mode` of Access Control that is used.
26    pub mode: String,
27}
28
29/// Configuration data for the server.
30#[derive(Debug)]
31pub struct Config {
32    /// The address and port the server should be started on.
33    pub bind_address: (String, u16),
34    /// The location of the database as a URL.
35    pub database_url: Secret<String>,
36    /// The discovery URI of the server that issues tokens.
37    ///
38    /// Can be used to fetch other relevant URLs such as the `jwks_uri` or the `token_endpoint`.
39    pub auth_discovery_uri: Url,
40    /// The `client_id` the frontend should use to log in its users.
41    pub client_id: String,
42
43    /// The URI of the auth server used to acquire a token.
44    pub auth_token_uri: Url,
45    /// The `client_id` the backend uses to communicate with the auth server.
46    pub auth_admin_client_id: Option<String>,
47    /// The `client_secret` the backend uses to communicate with the auth server.
48    pub auth_admin_client_secret: Option<Secret<String>>,
49    /// The `mode` of Access Control that is used.
50    pub mode: Mode,
51}
52
53/// Modes that the backend can be run in.
54#[derive(Debug, Clone, Copy, Deserialize)]
55pub enum Mode {
56    /// This mode indicates that the backend is run in a production environment.
57    Production,
58    /// This mode indicates that the backend is run in a testing environment.
59    Testing,
60}
61
62impl Mode {
63    ///
64    /// # Errors
65    /// * If the supplied String does not match any mode.
66    pub fn from_string(str: &str) -> Result<Self, &'static str> {
67        match str {
68            "testing" => Ok(Self::Testing),
69            "production" => {
70                if cfg!(not(feature = "access_control")) {
71                    return Err("feature access_control is disabled but mode production is set");
72                }
73                Ok(Self::Production)
74            }
75            _ => Err("invalid MODE in .env, should be testing or production"),
76        }
77    }
78}
79
80impl Config {
81    /// Load the configuration using environment variables.
82    ///
83    /// # Errors
84    /// * If the .env file is present, but there was an error loading it.
85    /// * If an environment variable is missing.
86    /// * If a variable could not be parsed correctly.
87    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
88        load_env_file()?;
89        let env: EnvVars = envy::from_env()?;
90
91        let auth_discovery_uri_str = format!(
92            "{}/realms/PermaplanT/.well-known/openid-configuration",
93            env.auth_host
94        );
95        let auth_discovery_uri = auth_discovery_uri_str.parse::<Url>().map_err(|e| {
96            format!("Failed to parse auth_discovery_uri: {e} (uri: {auth_discovery_uri_str})")
97        })?;
98        let auth_token_uri_str = format!(
99            "{}/realms/master/protocol/openid-connect/token",
100            env.auth_host
101        );
102        let auth_token_uri = auth_token_uri_str
103            .parse::<Url>()
104            .map_err(|e| format!("Failed to parse auth_token_uri: {e}"))?;
105
106        let mode = Mode::from_string(&env.mode).map_err(|err| format!("Error {err}"))?;
107
108        Ok(Self {
109            bind_address: (env.bind_address_host, env.bind_address_port),
110            database_url: Secret::new(env.database_url),
111            auth_discovery_uri,
112            client_id: env.auth_client_id,
113            auth_token_uri,
114            auth_admin_client_id: env.auth_admin_client_id,
115            auth_admin_client_secret: env.auth_admin_client_secret.map(Secret::new),
116            mode,
117        })
118    }
119}
120
121/// Load the .env file. A missing file does not result in an error.
122///
123/// # Errors
124/// * If the .env file is present, but there was an error loading it.
125fn load_env_file() -> Result<(), Box<dyn std::error::Error>> {
126    match dotenv() {
127        Err(e) if e.not_found() => Ok(()), // missing .env is ok
128        Err(e) => Err(e.into()),           // any other errors are a problem
129        _ => Ok(()),
130    }
131}