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 pub internal_networks: Vec<ipnetwork::IpNetwork>,
157 #[default("x-forwarded-for".to_string())]
158 pub ip_header: String,
159 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#[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}