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}