use std::{ path::{Path, PathBuf}, sync::mpsc::Sender, }; use pasetors::{keys::SymmetricKey, version4::V4}; use crate::services::UserConfirmationMessage; use super::AppError; #[derive(Clone)] pub struct Environment { assets_dir: PathBuf, database_url: String, email_sender: Sender, hostname: String, port: u32, rust_log: String, send_verification_email: bool, token_key: SymmetricKey, } impl Environment { pub fn init(email_sender: Sender) -> Result { let mut builder = EnvironmentObjectBuilder::new(email_sender); dotenvy::dotenv_iter() .expect("Missing .env file") .filter_map(|item| item.ok()) .for_each(|(key, value)| match key.as_str() { "ASSETS_DIR" => builder.with_assets_dir(value), "DATABASE_URL" => builder.with_database_url(value), "HOSTNAME" => builder.with_hostname(value), "PORT" => builder.with_port(value), "RUST_LOG" => builder.with_rust_log(value), "SEND_VERIFICATION_EMAIL" => builder.with_send_verification_email(value), "TOKEN_KEY" => builder.with_token_key(value), _ => {} }); let missing_vars = builder.uninitialized_variables(); if let Some(missing_vars) = missing_vars { Err(AppError::missing_environment_variables(missing_vars)) } else { Ok(Environment::from(builder)) } } pub fn hostname(&self) -> &str { self.hostname.as_str() } pub fn port(&self) -> u32 { self.port } pub fn token_key(&self) -> &SymmetricKey { &self.token_key } pub fn db_connection_uri(&self) -> &str { self.database_url.as_str() } pub fn assets_dir(&self) -> &Path { self.assets_dir.as_path() } pub fn email_sender(&self) -> &Sender { &self.email_sender } pub fn rust_log(&self) -> &str { self.rust_log.as_str() } pub fn send_verification_email(&self) -> bool { self.send_verification_email } } impl From for Environment { fn from(builder: EnvironmentObjectBuilder) -> Self { let EnvironmentObjectBuilder { assets_dir, database_url, email_sender, hostname, port, rust_log, send_verification, token_key, } = builder; Self { assets_dir: assets_dir.unwrap(), database_url: database_url.unwrap(), email_sender: email_sender.unwrap(), hostname: hostname.unwrap(), port: port.unwrap(), rust_log: rust_log.unwrap(), send_verification_email: send_verification.unwrap_or(true), token_key: token_key.unwrap(), } } } #[derive(Debug, Default)] pub struct EnvironmentObjectBuilder { pub assets_dir: Option, pub database_url: Option, pub email_sender: Option>, pub hostname: Option, pub port: Option, pub rust_log: Option, pub send_verification: Option, pub token_key: Option>, } impl EnvironmentObjectBuilder { pub fn new(email_sender: Sender) -> Self { Self { email_sender: Some(email_sender), ..Default::default() } } pub fn uninitialized_variables(&self) -> Option> { let mut missing_vars = [ ("HOSTNAME", self.hostname.as_deref()), ("DATABASE_URL", self.database_url.as_deref()), ("RUST_LOG", self.rust_log.as_deref()), ] .into_iter() .filter_map(|(key, value)| value.map(|_| key).xor(Some(key))) .collect::>(); if self.token_key.is_none() { missing_vars.push("TOKEN_KEY"); } if self.assets_dir.is_none() { missing_vars.push("ASSETS_DIR"); } if missing_vars.is_empty() { None } else { Some(missing_vars) } } pub fn with_hostname(&mut self, hostname: String) { self.hostname = Some(hostname); } pub fn with_port(&mut self, port: String) { let port = port .parse::() .inspect_err(|err| eprintln!("Not a valid port, defaulting to '42069': {err}")) .ok(); self.port = port; } pub fn with_token_key(&mut self, key: String) { match SymmetricKey::::try_from(key.as_str()).map_err(|_| AppError::token_key()) { Ok(key) => self.token_key = Some(key), Err(err) => panic!("{err}"), }; } pub fn with_database_url(&mut self, url: String) { self.database_url = Some(url); } pub fn with_assets_dir(&mut self, assets_dir_path: String) { let assets_dir = PathBuf::from(assets_dir_path); if !assets_dir.try_exists().unwrap_or_default() { panic!( "The 'ASSETS_DIR' environment variable points to a directory that doesn't exist." ); } if assets_dir.is_file() { panic!("The 'ASSETS_DIR' environment variable must be set to a directory, not a file."); } self.assets_dir = Some(assets_dir); } pub fn with_rust_log(&mut self, rust_log: String) { self.rust_log = Some(rust_log); } pub fn with_send_verification_email(&mut self, send_verification_email: String) { let send_verification_email = send_verification_email.to_lowercase() == "true"; self.send_verification = Some(send_verification_email); } }