//! Configuration, from the environment only. Secrets never have a default. use std::path::PathBuf; #[derive(Clone, Debug)] pub struct Connect { /// Base URL of the sso-connect instance (no trailing slash). pub base_url: String, pub client_id: String, pub client_secret: String, } #[derive(Clone, Debug)] pub struct Publish { /// Remote of the texts repository, e.g. `gitea@git.open.us.org:yvv/decision-depot-textes-reference.git`. pub remote: String, pub branch: String, /// Private SSH key allowed to push there (deploy key). pub key: PathBuf, pub author_name: String, pub author_email: String, } #[derive(Clone, Debug)] pub struct Config { pub bind: String, pub data_dir: PathBuf, pub web_dir: PathBuf, pub seed_dir: PathBuf, /// Public origin, e.g. `https://decision.librodrome.org` — callbacks and cookies. pub public_url: String, pub connect: Option, pub admin_g1_ids: Vec, pub dev_login: bool, pub bootstrap_admin: bool, pub publish: Option, pub rpc_endpoints: Vec, pub indexers: Vec, pub wot_fallback: u32, pub build: String, } fn var(name: &str) -> Option { std::env::var(name).ok().map(|v| v.trim().to_owned()).filter(|v| !v.is_empty()) } fn flag(name: &str) -> bool { var(name).is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on")) } fn list(name: &str, default: &[&str]) -> Vec { match var(name) { Some(v) => v.split(',').map(|s| s.trim().to_owned()).filter(|s| !s.is_empty()).collect(), None => default.iter().map(|s| (*s).to_owned()).collect(), } } impl Config { pub fn from_env() -> Self { let data_dir = PathBuf::from(var("LD_DATA_DIR").unwrap_or_else(|| "data".into())); let connect = match (var("LD_CONNECT_CLIENT_ID"), var("LD_CONNECT_CLIENT_SECRET")) { (Some(client_id), Some(client_secret)) => Some(Connect { base_url: var("LD_CONNECT_URL") .unwrap_or_else(|| "https://connect.monnaie-libre.fr".into()) .trim_end_matches('/') .to_owned(), client_id, client_secret, }), _ => None, }; let publish = var("LD_PUBLISH_REMOTE").map(|remote| Publish { remote, branch: var("LD_PUBLISH_BRANCH").unwrap_or_else(|| "main".into()), key: var("LD_PUBLISH_KEY").map(PathBuf::from).unwrap_or_else(|| data_dir.join("keys/publish_ed25519")), author_name: var("LD_PUBLISH_AUTHOR").unwrap_or_else(|| "libreDecision".into()), author_email: var("LD_PUBLISH_EMAIL").unwrap_or_else(|| "decision@librodrome.org".into()), }); Self { bind: var("LD_BIND").unwrap_or_else(|| "0.0.0.0:8002".into()), web_dir: PathBuf::from(var("LD_WEB_DIR").unwrap_or_else(|| "web/build".into())), seed_dir: PathBuf::from(var("LD_SEED_DIR").unwrap_or_else(|| "seeds".into())), public_url: var("LD_PUBLIC_URL") .unwrap_or_else(|| "http://127.0.0.1:3002".into()) .trim_end_matches('/') .to_owned(), connect, admin_g1_ids: list("LD_ADMIN_G1_IDS", &[]), dev_login: flag("LD_DEV_LOGIN"), bootstrap_admin: flag("LD_BOOTSTRAP_ADMIN"), publish, rpc_endpoints: list( "LD_RPC_ENDPOINTS", &[ "https://rpc.duniter.org", "https://g1.axiom-team.fr/ws/", "https://g1.asycn.io/ws/", "https://g1.coinduf.eu", ], ), indexers: list( "LD_INDEXERS", &["https://indexer.duniter.org/v1/graphql", "https://g1-squid.axiom-team.fr/v1/graphql"], ), wot_fallback: var("LD_WOT_FALLBACK").and_then(|v| v.parse().ok()).unwrap_or(5745), build: var("LD_BUILD").unwrap_or_else(|| "dev".into()), data_dir, } } /// Cookies are `Secure` as soon as the public origin is https. pub fn secure_cookies(&self) -> bool { self.public_url.starts_with("https://") } pub fn connect_callback(&self) -> String { format!("{}/api/auth/g1/callback", self.public_url) } /// For tests: an in-memory configuration with dev login. pub fn for_tests() -> Self { Self { bind: "127.0.0.1:0".into(), data_dir: std::env::temp_dir().join("ld-tests"), web_dir: PathBuf::from("web/build"), seed_dir: PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../seeds")), public_url: "http://127.0.0.1:3002".into(), connect: None, admin_g1_ids: vec!["idty:8352".into()], dev_login: true, bootstrap_admin: false, publish: None, rpc_endpoints: vec![], indexers: vec![], wot_fallback: 5745, build: "test".into(), } } }