Skip to main content

web/
config.rs

1use server::config::{ConfigError, system_config};
2
3/// Public-facing SSH endpoint advertised in the
4/// `/account/ssh-key` "key added" snippet. All three values are
5/// optional — operators set them via env (`SSH_HOSTNAME`,
6/// `SSH_PORT`, `SSH_HOST_FINGERPRINT`) on the deployment ConfigMap.
7#[derive(Debug, Clone)]
8pub struct SshConnectInfo {
9    pub hostname: String,
10    pub port: u16,
11    pub host_fingerprint: String,
12}
13
14impl SshConnectInfo {
15    fn from_env() -> Self {
16        Self {
17            hostname: std::env::var("SSH_HOSTNAME")
18                .unwrap_or_else(|_| "ssh.example.invalid".to_string()),
19            port: std::env::var("SSH_PORT")
20                .ok()
21                .and_then(|raw| raw.parse::<u16>().ok())
22                .unwrap_or(2222),
23            host_fingerprint: std::env::var("SSH_HOST_FINGERPRINT")
24                .unwrap_or_else(|_| "(not configured)".to_string()),
25        }
26    }
27}
28
29/// An environment value, if the deployment actually supplied one.
30///
31/// A key set to nothing counts as unset: a ConfigMap can carry an empty
32/// string, and taking that literally would override good configuration with a
33/// URL that cannot parse.
34///
35/// Takes the looked-up value rather than reading the environment so it is
36/// testable without mutating process-global state.
37pub fn env_override(raw: Option<String>) -> Option<String> {
38    raw.filter(|value| !value.trim().is_empty())
39}
40
41#[derive(Debug, Clone)]
42pub struct Config {
43    pub site_url: String,
44
45    pub redis_url: String,
46
47    // Token lifetimes only. Signing keys are no longer global config — each user
48    // signs with their own keypair (see `crate::auth_keys` / `server::auth_keys`).
49    pub access_token_max_age: i64,
50
51    pub refresh_token_max_age: i64,
52
53    pub ssh: SshConnectInfo,
54}
55
56impl Config {
57    pub async fn init() -> Result<Config, ConfigError> {
58        let site_url = system_config("site_url")
59            .await?
60            .ok_or(ConfigError::NoConfig("site_url".to_string()))?
61            .to_string();
62
63        // The environment wins for the session store: it is a deployment
64        // endpoint, like DATABASE_URL, not something an operator edits in the
65        // running site. The seeded row is redis://127.0.0.1:6379/, so without
66        // this every fresh install crash-loops on any host where Redis is not
67        // on loopback, and the only cure is rewriting the row by hand after
68        // the first boot.
69        let redis_url = match env_override(std::env::var("REDIS_URL").ok()) {
70            Some(url) => url,
71            None => system_config("redis_url")
72                .await?
73                .ok_or(ConfigError::NoConfig("redis_url".to_string()))?
74                .to_string(),
75        };
76
77        let access_token_max_age = system_config("access_token_maxage")
78            .await?
79            .ok_or(ConfigError::NoConfig("access_token_maxage".to_string()))?
80            .to_string();
81
82        let refresh_token_max_age = system_config("refresh_token_maxage")
83            .await?
84            .ok_or(ConfigError::NoConfig("refresh_token_maxage".to_string()))?
85            .to_string();
86
87        let parse_age = |raw: String, field: &str| {
88            raw.parse::<i64>()
89                .map_err(|_| ConfigError::NoConfig(field.to_string()))
90        };
91
92        Ok(Config {
93            site_url,
94            redis_url,
95            access_token_max_age: parse_age(access_token_max_age, "access_token_maxage")?,
96            refresh_token_max_age: parse_age(refresh_token_max_age, "refresh_token_maxage")?,
97            ssh: SshConnectInfo::from_env(),
98        })
99    }
100}