use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use serde::Deserialize; /// Enable secure cookies by default only in release mode /// (Secure cookies can't be set over HTTP. Rough assumption: development is done over HTTP) const DEFAULT_COOKIES_SECURE: bool = cfg!(not(debug_assertions)); // Simple newtype to avoid showing secrets with `Debug` / `Display` #[derive(Clone, Deserialize)] pub struct SecretString(String); impl SecretString { pub fn expose(&self) -> &String { &self.0 } } impl From for SecretString { fn from(s: String) -> Self { Self(s) } } impl std::fmt::Debug for SecretString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "*****") } } impl std::fmt::Display for SecretString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "*****") } } #[derive(Debug, Clone, Deserialize)] pub struct AuthConfig { pub open_signup: bool, pub cookies_secure: bool, } /// Build a connection URI from parts fn format_uri( scheme: &str, username: Option<&String>, password: Option<&String>, host: &str, port: Option, path: Option<&String>, ) -> String { let mut url = format!("{scheme}://"); if let Some(username) = username { url.push_str(username); if let Some(password) = password { url.push_str(&format!(":{password}")); } url.push('@'); } url.push_str(host); if let Some(port) = port { url.push_str(&format!(":{port}")); } if let Some(path) = path { url.push_str(&format!("/{path}")); } url } #[derive(Debug, Clone, Deserialize)] pub struct DatabaseConfig { #[serde(flatten)] connection: DatabaseConnectionConfig, } impl DatabaseConfig { /// Get the configured database connection URI pub fn connection_uri(&self) -> String { self.connection.as_uri() } } #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] enum DatabaseConnectionConfig { FromUrl { url: SecretString, }, FromParts { host: String, port: Option, database: Option, username: Option, password: Option, }, } impl DatabaseConnectionConfig { /// Convert this configuration into the Postgres connection URI pub fn as_uri(&self) -> String { match self { Self::FromUrl { url } => url.expose().clone(), Self::FromParts { host, port, database, username, password, } => format_uri( "postgres", username.as_ref(), password.as_ref().map(|s| s.expose()), host, *port, database.as_ref(), ), } } } #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] enum KeyValStoreConnectionConfig { FromUrl { url: SecretString, }, FromParts { scheme: Option, host: String, port: Option, database: Option, username: Option, password: Option, }, } impl KeyValStoreConnectionConfig { /// Convert this configuration into the Redis connection URI pub fn as_uri(&self) -> String { match self { Self::FromUrl { url } => url.expose().clone(), Self::FromParts { scheme, host, port, database, username, password, } => format_uri( scheme.as_deref().unwrap_or("redis"), username.as_ref(), password.as_ref().map(|s| s.expose().clone()).as_ref(), host, *port, database.as_ref(), ), } } } #[derive(Debug, Clone, Deserialize)] pub struct KeyValStoreConfig { #[serde(flatten)] connection: KeyValStoreConnectionConfig, } impl KeyValStoreConfig { /// Get the configured database connection URI pub fn connection_uri(&self) -> String { self.connection.as_uri() } } #[derive(Debug, Clone, Deserialize)] pub struct ServerConfig { pub public_path: PathBuf, pub host: IpAddr, pub port: u16, } impl ServerConfig { pub fn serve_addr(&self) -> SocketAddr { SocketAddr::new(self.host, self.port) } } #[derive(Debug, Clone, Deserialize)] /// Top-level application configuration pub struct Config { pub auth: AuthConfig, pub database: DatabaseConfig, pub key_val_store: KeyValStoreConfig, pub server: ServerConfig, } /// Parse configuration from the expected files and environment variables pub fn load_config() -> Result { use config::{Environment, File}; let pkg_name = env!("CARGO_PKG_NAME"); config::Config::builder() .set_default( "server.port", dioxus::cli_config::server_port().unwrap_or(8080), )? .set_default( "server.host", dioxus::cli_config::server_ip() .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) .to_string(), )? .set_default("auth.open_signup", false)? .set_default("auth.cookies_secure", DEFAULT_COOKIES_SECURE)? .set_default("server.public_path", default_public_dir())? .add_source(File::with_name(&format!("/etc/{pkg_name}/config")).required(false)) .add_source(File::with_name(&format!("/etc/{pkg_name}")).required(false)) .add_source(File::with_name("config").required(false)) .add_source(Environment::with_prefix(pkg_name).separator("_")) .build()? .try_deserialize() } /// Provide a sane default for the public path, using the same sources as Dioxus does internally. /// Checks the `DIOXUS_PUBLIC_PATH` environment variable, then tries relative to the path of this /// executable fn default_public_dir() -> Option { std::env::var("DIOXUS_PUBLIC_PATH").ok().or_else(|| { std::env::current_exe().ok().and_then(|path| { path.parent() .expect("current executable path must have a parent") .join("public") .into_os_string() .into_string() .ok() }) }) }