scufflecloud_big_bin/
config.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use anyhow::Context;
6use fred::prelude::ClientLike;
7
8#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
9#[serde(default)]
10pub(crate) struct Config {
11    #[default(env!("CARGO_PKG_NAME").to_string())]
12    pub service_name: String,
13    #[default(SocketAddr::from(([127, 0, 0, 1], 3001)))]
14    pub core_bind: SocketAddr,
15    #[default(SocketAddr::from(([127, 0, 0, 1], 3003)))]
16    pub email_bind: SocketAddr,
17    #[default = "info"]
18    pub level: String,
19    #[default(None)]
20    pub db_url: Option<String>,
21    #[default(false)]
22    pub swagger_ui: bool,
23    #[default = "scuffle.cloud"]
24    pub rp_id: String,
25    #[default(url::Url::from_str("https://dashboard.scuffle.cloud").unwrap())]
26    pub dashboard_origin: url::Url,
27    #[default = "1x0000000000000000000000000000000AA"]
28    pub turnstile_secret_key: String,
29    pub timeouts: TimeoutConfig,
30    pub google_oauth2: GoogleOAuth2Config,
31    pub telemetry: Option<TelemetryConfig>,
32    pub redis: RedisConfig,
33    #[default = "Scuffle"]
34    pub email_from_name: String,
35    #[default = "no-reply@scuffle.cloud"]
36    pub email_from_address: String,
37    pub reverse_proxy: Option<ReverseProxyConfig>,
38    #[default("./GeoLite2-City.mmdb".parse().unwrap())]
39    pub maxminddb_path: PathBuf,
40    pub aws: AwsConfig,
41    pub mtls: MtlsConfig,
42}
43
44scuffle_settings::bootstrap!(Config);
45
46const fn days(days: u64) -> std::time::Duration {
47    hours(days * 24)
48}
49
50const fn hours(hours: u64) -> std::time::Duration {
51    minutes(hours * 60)
52}
53
54const fn minutes(mins: u64) -> std::time::Duration {
55    std::time::Duration::from_secs(mins * 60)
56}
57
58#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
59#[serde(default)]
60pub(crate) struct TimeoutConfig {
61    #[default(minutes(2))]
62    pub max_request_lifetime: std::time::Duration,
63    #[default(days(30))]
64    pub user_session: std::time::Duration,
65    #[default(minutes(5))]
66    pub mfa: std::time::Duration,
67    #[default(hours(4))]
68    pub user_session_token: std::time::Duration,
69    #[default(hours(1))]
70    pub new_user_email_request: std::time::Duration,
71    #[default(minutes(5))]
72    pub user_session_request: std::time::Duration,
73    #[default(minutes(15))]
74    pub magic_link_request: std::time::Duration,
75}
76
77#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
78pub(crate) struct GoogleOAuth2Config {
79    pub client_id: String,
80    pub client_secret: String,
81}
82
83#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
84pub(crate) struct TelemetryConfig {
85    #[default("[::1]:4317".parse().unwrap())]
86    pub bind: SocketAddr,
87}
88
89#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
90pub(crate) struct RedisConfig {
91    #[default(vec!["localhost:6379".to_string()])]
92    pub servers: Vec<String>,
93    #[default(None)]
94    pub username: Option<String>,
95    #[default(None)]
96    pub password: Option<String>,
97    #[default(0)]
98    pub database: u8,
99    #[default(10)]
100    pub pool_size: usize,
101}
102
103fn parse_server(server: &str) -> anyhow::Result<fred::types::config::Server> {
104    let port_ip = server.split(':').collect::<Vec<_>>();
105
106    if port_ip.len() == 1 {
107        Ok(fred::types::config::Server::new(port_ip[0], 6379))
108    } else {
109        Ok(fred::types::config::Server::new(
110            port_ip[0],
111            port_ip[1].parse::<u16>().context("invalid port")?,
112        ))
113    }
114}
115
116impl RedisConfig {
117    pub(crate) async fn setup(&self) -> anyhow::Result<fred::clients::Pool> {
118        let redis_server_config = if self.servers.len() == 1 {
119            fred::types::config::ServerConfig::Centralized {
120                server: parse_server(&self.servers[0])?,
121            }
122        } else {
123            fred::types::config::ServerConfig::Clustered {
124                hosts: self
125                    .servers
126                    .iter()
127                    .map(|s| parse_server(s))
128                    .collect::<anyhow::Result<Vec<_>>>()?,
129                policy: Default::default(),
130            }
131        };
132
133        tracing::info!(config = ?redis_server_config, "connecting to redis");
134
135        let config = fred::types::config::Config {
136            server: redis_server_config,
137            database: Some(self.database),
138            fail_fast: true,
139            password: self.password.clone(),
140            username: self.username.clone(),
141            ..Default::default()
142        };
143
144        let client = fred::clients::Pool::new(config, None, None, None, self.pool_size).context("redis pool")?;
145        client.init().await?;
146
147        Ok(client)
148    }
149}
150
151#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
152pub(crate) struct ReverseProxyConfig {
153    /// List of networks that bypass the IP address extraction from the configured IP header.
154    /// These are typically internal networks and other services that directly connect to the server without going
155    /// through the reverse proxy.
156    pub internal_networks: Vec<ipnetwork::IpNetwork>,
157    #[default("x-forwarded-for".to_string())]
158    pub ip_header: String,
159    /// List of trusted proxy networks that the server accepts connections from.
160    /// These are typically the networks of the reverse proxies in front of the server, e.g. Cloudflare, etc.
161    pub trusted_proxies: Vec<ipnetwork::IpNetwork>,
162}
163
164#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
165pub(crate) struct AwsConfig {
166    #[default = "us-east-1"]
167    pub region: String,
168    pub access_key_id: String,
169    pub secret_access_key: String,
170}
171
172// TODO: Remove mTLS from this binary once we don't use a real connection anymore.
173#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)]
174pub(crate) struct MtlsConfig {
175    pub root_cert_path: PathBuf,
176    pub core_cert_path: PathBuf,
177    pub core_key_path: PathBuf,
178    pub email_cert_path: PathBuf,
179    pub email_key_path: PathBuf,
180}