use std::{path::Path, sync::OnceLock}; use config::Config; use serde::Deserialize; #[derive(Deserialize, Clone)] pub struct ServerConfig { pub address: Option, pub port: Option, /// Public origin the browser reaches the app on. The OIDC redirect uri is /// built from it, so it must match what is registered on the provider. /// In development this is the vite dev server, not the backend. pub base_url: Option, } #[derive(Deserialize, Clone)] pub struct PostgresConfig { pub host: Option, pub port: Option, pub user: String, pub password: String, pub name: String, } /// Whiskey, the AGEPoly OIDC provider #[derive(Deserialize, Clone)] pub struct OidcConfig { pub issuer_url: String, pub client_id: String, pub client_secret: String, pub session_lifetime: Option, } /// A user that can be logged in without going through the provider. /// Only usable in debug builds, see `POST /api/login`. #[derive(Deserialize, Clone)] pub struct DevUserConfig { pub firstname: String, pub name: String, pub email: String, pub external_id: Option, #[serde(default)] pub units: Vec, #[serde(default)] pub admin: bool, } #[derive(Deserialize, Clone)] pub struct AppConfig { pub server: ServerConfig, pub postgres: PostgresConfig, pub oidc: OidcConfig, #[serde(default)] pub dev_users: Vec, /// Directory containing the built frontend pub frontend_dir: String, } impl AppConfig { pub fn get_bind_address(&self) -> String { let address = self.server.address.clone().unwrap_or("0.0.0.0".to_owned()); let port = self.server.port.unwrap_or(3000); format!("{}:{}", address, port) } /// Origin used to build the OIDC redirect uri. Defaults to the vite dev /// server, which is what a developer reaches the app on. pub fn get_base_url(&self) -> String { self.server .base_url .clone() .unwrap_or("http://localhost:5000".to_owned()) } /// Session lifetime in minutes pub fn get_session_lifetime(&self) -> i64 { self.oidc.session_lifetime.unwrap_or(60) } pub fn get_postgresql_url(&self) -> String { let host = self.postgres.host.clone().unwrap_or("localhost".to_owned()); let port = self.postgres.port.unwrap_or(5432); let user = self.postgres.user.clone(); let password = self.postgres.password.clone(); let name = self.postgres.name.clone(); format!("postgres://{user}:{password}@{host}:{port}/{name}") } } /// Loads the configuration, by decreasing priority: /// - `APP__SERVER__PORT=3000` style environment variables /// - `./config.yml` /// - the file pointed by `$APP_CONFIG` (`/etc/cargagep/config.yml` by default) pub fn get() -> &'static AppConfig { static CONFIG: OnceLock = OnceLock::new(); CONFIG.get_or_init(|| { let config_path = std::env::var("APP_CONFIG").unwrap_or("/etc/cargagep/config.yml".to_owned()); let config_path = Path::new(&config_path); let raw = Config::builder() .add_source(config::File::from(config_path).required(false)) .add_source(config::File::with_name("config").required(false)) .add_source( config::Environment::with_prefix("APP") .separator("__") .ignore_empty(true), ) .build() .expect("Invalid config source"); raw.try_deserialize::() .expect("Invalid config format") }) }