1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//! Configuration of the server.

use dotenvy::dotenv;
use reqwest::Url;
use secrecy::Secret;
use serde::Deserialize;

/// Environment variables that are required for the configuration of the server.
#[derive(Deserialize)]
struct EnvVars {
    /// The host the server should be started on.
    pub bind_address_host: String,
    /// The port the server should be started on.
    pub bind_address_port: u16,
    /// The connection string for the database.
    pub database_url: String,
    /// The host of the authentication server.
    pub auth_host: String,
    /// The `client_id` the frontend should use to log in its users.
    pub auth_client_id: String,
    /// The `client_id` the backend uses to communicate with the auth server.
    pub auth_admin_client_id: Option<String>,
    /// The `client_secret` the backend uses to communicate with the auth server.
    pub auth_admin_client_secret: Option<String>,
}

/// Configuration data for the server.
#[derive(Debug)]
pub struct Config {
    /// The address and port the server should be started on.
    pub bind_address: (String, u16),
    /// The location of the database as a URL.
    pub database_url: Secret<String>,
    /// The discovery URI of the server that issues tokens.
    ///
    /// Can be used to fetch other relevant URLs such as the `jwks_uri` or the `token_endpoint`.
    pub auth_discovery_uri: Url,
    /// The `client_id` the frontend should use to log in its users.
    pub client_id: String,

    /// The URI of the auth server used to acquire a token.
    pub auth_token_uri: Url,
    /// The `client_id` the backend uses to communicate with the auth server.
    pub auth_admin_client_id: Option<String>,
    /// The `client_secret` the backend uses to communicate with the auth server.
    pub auth_admin_client_secret: Option<Secret<String>>,
}

impl Config {
    /// Load the configuration using environment variables.
    ///
    /// # Errors
    /// * If the .env file is present, but there was an error loading it.
    /// * If an environment variable is missing.
    /// * If a variable could not be parsed correctly.
    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
        load_env_file()?;
        let env: EnvVars = envy::from_env()?;

        let auth_discovery_uri_str = format!(
            "{}/realms/PermaplanT/.well-known/openid-configuration",
            env.auth_host
        );
        let auth_discovery_uri = auth_discovery_uri_str.parse::<Url>().map_err(|e| {
            format!("Failed to parse auth_discovery_uri: {e} (uri: {auth_discovery_uri_str})")
        })?;
        let auth_token_uri_str = format!(
            "{}/realms/master/protocol/openid-connect/token",
            env.auth_host
        );
        let auth_token_uri = auth_token_uri_str
            .parse::<Url>()
            .map_err(|e| format!("Failed to parse auth_token_uri: {e}"))?;

        Ok(Self {
            bind_address: (env.bind_address_host, env.bind_address_port),
            database_url: Secret::new(env.database_url),
            auth_discovery_uri,
            client_id: env.auth_client_id,
            auth_token_uri,
            auth_admin_client_id: env.auth_admin_client_id,
            auth_admin_client_secret: env.auth_admin_client_secret.map(Secret::new),
        })
    }
}

/// Load the .env file. A missing file does not result in an error.
///
/// # Errors
/// * If the .env file is present, but there was an error loading it.
fn load_env_file() -> Result<(), Box<dyn std::error::Error>> {
    match dotenv() {
        Err(e) if e.not_found() => Ok(()), // missing .env is ok
        Err(e) => Err(e.into()),           // any other errors are a problem
        _ => Ok(()),
    }
}