Skip to main content

lockbook_server_lib/
config.rs

1use crate::config::Environment::{Local, Prod, Unknown};
2use lb_rs::model::account::Username;
3use semver::VersionReq;
4use std::collections::HashSet;
5use std::fmt::Display;
6use std::path::PathBuf;
7use std::time::Duration;
8use std::{env, fmt, fs};
9
10#[derive(Clone, Debug)]
11pub struct Config {
12    pub server: ServerConfig,
13    pub index_db: IndexDbConf,
14    pub files: FilesConfig,
15    pub metrics: MetricsConfig,
16    pub billing: BillingConfig,
17    pub admin: AdminConfig,
18    pub features: FeatureFlags,
19}
20
21impl Config {
22    pub fn from_env_vars() -> Self {
23        Self {
24            index_db: IndexDbConf::from_env_vars(),
25            files: FilesConfig::from_env_vars(),
26            server: ServerConfig::from_env_vars(),
27            metrics: MetricsConfig::from_env_vars(),
28            billing: BillingConfig::from_env_vars(),
29            admin: AdminConfig::from_env_vars(),
30            features: FeatureFlags::from_env_vars(),
31        }
32    }
33
34    pub fn is_prod(&self) -> bool {
35        self.server.env == Prod
36    }
37}
38
39#[derive(Clone, Debug)]
40pub struct IndexDbConf {
41    pub db_location: String,
42    pub time_between_compacts: Duration,
43}
44
45impl IndexDbConf {
46    pub fn from_env_vars() -> Self {
47        Self {
48            db_location: env_or_panic("INDEX_DB_LOCATION"),
49            time_between_compacts: Duration::from_secs(
50                env_or_panic("MINUTES_BETWEEN_BACKGROUND_COMPACTS")
51                    .parse::<u64>()
52                    .unwrap()
53                    * 60,
54            ),
55        }
56    }
57}
58
59#[derive(Clone, Debug, Default)]
60pub struct AdminConfig {
61    pub admins: HashSet<Username>,
62}
63
64impl AdminConfig {
65    pub fn from_env_vars() -> Self {
66        Self {
67            admins: env::var("ADMINS")
68                .unwrap_or_else(|_| "".to_string())
69                .split(", ")
70                .map(|part| part.to_string())
71                .collect(),
72        }
73    }
74}
75
76#[derive(Clone, Debug)]
77pub struct FeatureFlags {
78    pub new_accounts: bool,
79    pub new_account_rate_limit: bool,
80    pub bandwidth_controls: bool,
81}
82
83impl FeatureFlags {
84    pub fn from_env_vars() -> Self {
85        Self {
86            new_accounts: env::var("FEATURE_NEW_ACCOUNTS")
87                .unwrap_or_else(|_| "true".to_string())
88                .parse()
89                .unwrap(),
90            new_account_rate_limit: env::var("FEATURE_NEW_ACCOUNT_LIMITS")
91                .unwrap_or_else(|_| "false".to_string())
92                .parse()
93                .unwrap(),
94            bandwidth_controls: env::var("FEATURE_BANDWIDTH_CONTROLS")
95                .unwrap_or_else(|_| "true".to_string())
96                .parse()
97                .unwrap(),
98        }
99    }
100}
101
102#[derive(Clone, Debug)]
103pub struct FilesConfig {
104    pub path: PathBuf,
105}
106
107impl FilesConfig {
108    pub fn from_env_vars() -> Self {
109        let path = env_or_panic("FILES_PATH");
110        let path = PathBuf::from(path);
111        fs::create_dir_all(&path).unwrap();
112        Self { path }
113    }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub enum Environment {
118    Prod,
119    Local,
120    Unknown,
121}
122
123impl Environment {
124    pub fn from_env_vars() -> Self {
125        match env::var("ENVIRONMENT") {
126            Ok(var) => match var.to_lowercase().as_str() {
127                "production" | "prod" => Prod,
128                "local" | "localhost" => Local,
129                _ => Unknown,
130            },
131            Err(_) => Unknown,
132        }
133    }
134}
135
136impl Display for Environment {
137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138        write!(f, "{:?}", &self)
139    }
140}
141
142#[derive(Clone, Debug)]
143pub struct ServerConfig {
144    pub env: Environment,
145    pub port: u16,
146    pub max_auth_delay: u128,
147    pub log_path: String,
148    pub pd_api_key: Option<String>,
149    pub discord_webhook_url: Option<String>,
150    pub ssl_cert_location: Option<String>,
151    pub ssl_private_key_location: Option<String>,
152    pub min_core_version: VersionReq,
153}
154
155impl ServerConfig {
156    pub fn from_env_vars() -> Self {
157        let env = Environment::from_env_vars();
158        let port = env_or_panic("SERVER_PORT").parse().unwrap();
159        let max_auth_delay = env_or_panic("MAX_AUTH_DELAY").parse().unwrap();
160        let log_path = env_or_panic("LOG_PATH").parse().unwrap();
161        let pd_api_key = env_or_empty("PD_KEY");
162        let discord_webhook_url = env_or_empty("DISCORD_WEBHOOK_URL");
163        let ssl_cert_location = env_or_empty("SSL_CERT_LOCATION");
164        let ssl_private_key_location = env_or_empty("SSL_PRIVATE_KEY_LOCATION");
165        let min_core_version = VersionReq::parse(&env_or_panic("MIN_CORE_VERSION")).unwrap();
166
167        match (&discord_webhook_url, &pd_api_key, &ssl_cert_location, &ssl_private_key_location) {
168            (Some(_), Some(_), Some(_), Some(_)) | (None, None, None, None) => {}
169            _ => panic!(
170                "Invalid config, discord & pd & ssl must all be Some (production) or all be None (local)"
171            ),
172        }
173
174        Self {
175            env,
176            port,
177            max_auth_delay,
178            log_path,
179            pd_api_key,
180            discord_webhook_url,
181            ssl_cert_location,
182            ssl_private_key_location,
183            min_core_version,
184        }
185    }
186}
187
188#[derive(Clone, Debug)]
189pub struct MetricsConfig {
190    pub time_between_metrics_refresh: Duration,
191    pub time_between_metrics: Duration,
192}
193
194impl MetricsConfig {
195    pub fn from_env_vars() -> Self {
196        Self {
197            time_between_metrics_refresh: Duration::from_secs(
198                env_or_panic("MINUTES_BETWEEN_METRICS_REFRESH")
199                    .parse::<u64>()
200                    .unwrap()
201                    * 60,
202            ),
203            time_between_metrics: Duration::from_millis(
204                env_or_panic("MILLIS_BETWEEN_METRICS")
205                    .parse::<u64>()
206                    .unwrap(),
207            ),
208        }
209    }
210}
211
212#[derive(Clone, Debug)]
213pub struct BillingConfig {
214    pub millis_between_user_payment_flows: u64,
215    pub time_between_lock_attempts: Duration,
216    pub google: GoogleConfig,
217    pub stripe: StripeConfig,
218    pub apple: AppleConfig,
219}
220
221impl BillingConfig {
222    pub fn from_env_vars() -> Self {
223        Self {
224            millis_between_user_payment_flows: env_or_panic("MILLIS_BETWEEN_PAYMENT_FLOWS")
225                .parse()
226                .unwrap(),
227            time_between_lock_attempts: Duration::from_secs(
228                env_or_panic("MILLIS_BETWEEN_LOCK_ATTEMPTS")
229                    .parse::<u64>()
230                    .unwrap(),
231            ),
232            google: GoogleConfig::from_env_vars(),
233            stripe: StripeConfig::from_env_vars(),
234            apple: AppleConfig::from_env_vars(),
235        }
236    }
237}
238
239#[derive(Clone, Debug)]
240pub struct AppleConfig {
241    pub iap_key: String,
242    pub iap_key_id: String,
243    pub asc_public_key: String,
244    pub issuer_id: String,
245    pub subscription_product_id: String,
246    pub asc_shared_secret: String,
247    pub apple_root_cert: Vec<u8>,
248    pub monthly_sub_group_id: String,
249}
250
251impl AppleConfig {
252    pub fn from_env_vars() -> Self {
253        let is_apple_prod = env_or_empty("IS_APPLE_PROD")
254            .map(|is_apple_prod| is_apple_prod.parse().unwrap())
255            .unwrap_or(false);
256
257        let apple_root_cert =
258            env_or_empty("APPLE_ROOT_CERT_PATH").map(|cert_path| fs::read(cert_path).unwrap());
259        let apple_iap_key = env_or_empty("APPLE_IAP_KEY_PATH")
260            .map(|key_path| fs::read_to_string(key_path).unwrap());
261        let apple_asc_pub_key = env_or_empty("APPLE_ASC_PUB_KEY_PATH")
262            .map(|key_path| fs::read_to_string(key_path).unwrap());
263
264        Self {
265            iap_key: if is_apple_prod {
266                apple_iap_key.unwrap()
267            } else {
268                apple_iap_key.unwrap_or_default()
269            },
270            iap_key_id: env_or_panic("APPLE_IAP_KEY_ID"),
271            asc_public_key: if is_apple_prod {
272                apple_asc_pub_key.unwrap()
273            } else {
274                apple_asc_pub_key.unwrap_or_default()
275            },
276            issuer_id: env_or_panic("APPLE_ISSUER_ID"),
277            subscription_product_id: env_or_panic("APPLE_SUB_PROD_ID"),
278            asc_shared_secret: env_or_panic("APPLE_ASC_SHARED_SECRET"),
279            apple_root_cert: if is_apple_prod {
280                apple_root_cert.unwrap()
281            } else {
282                apple_root_cert.unwrap_or_default()
283            },
284            monthly_sub_group_id: env_or_panic("APPLE_MONTHLY_SUB_GROUP_ID"),
285        }
286    }
287}
288
289#[derive(Clone, Debug)]
290pub struct GoogleConfig {
291    pub service_account_key: Option<String>,
292    pub premium_subscription_product_id: String,
293    pub premium_subscription_offer_id: String,
294    pub pubsub_token: String,
295}
296
297impl GoogleConfig {
298    pub fn from_env_vars() -> Self {
299        Self {
300            service_account_key: env_or_empty("GOOGLE_CLOUD_SERVICE_ACCOUNT_KEY"),
301            premium_subscription_product_id: env_or_panic(
302                "GOOGLE_PLAY_PREMIUM_SUBSCRIPTION_PRODUCT_ID",
303            ),
304            premium_subscription_offer_id: env_or_panic(
305                "GOOGLE_PLAY_PREMIUM_SUBSCRIPTION_OFFER_ID",
306            ),
307            pubsub_token: env_or_panic("GOOGLE_CLOUD_PUBSUB_NOTIFICATION_TOKEN"),
308        }
309    }
310}
311
312#[derive(Clone, Debug)]
313pub struct StripeConfig {
314    pub stripe_secret: String,
315    pub signing_secret: String,
316    pub premium_price_id: String,
317}
318
319impl StripeConfig {
320    pub fn from_env_vars() -> Self {
321        Self {
322            stripe_secret: env_or_panic("STRIPE_SECRET").parse().unwrap(),
323            signing_secret: env_or_panic("STRIPE_SIGNING_SECRET").parse().unwrap(),
324            premium_price_id: env_or_panic("STRIPE_PREMIUM_PRICE_ID").parse().unwrap(),
325        }
326    }
327}
328
329fn env_or_panic(var_name: &str) -> String {
330    env::var(var_name).unwrap_or_else(|_| panic!("Missing environment variable {var_name}"))
331}
332
333fn env_or_empty(var_name: &str) -> Option<String> {
334    env::var(var_name).ok()
335}