2024-08-06 11:08:15 -04:00
|
|
|
use std::{
|
|
|
|
collections::HashSet,
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
sync::mpsc::Sender,
|
|
|
|
};
|
|
|
|
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
use pasetors::{keys::SymmetricKey, version4::V4};
|
|
|
|
|
2024-08-24 13:22:51 -04:00
|
|
|
use crate::services::UserConfirmationMessage;
|
|
|
|
|
|
|
|
use super::AppError;
|
|
|
|
|
|
|
|
static REQUIRED_ENV_VARS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
|
|
|
[
|
|
|
|
"HOSTNAME",
|
|
|
|
"PORT",
|
|
|
|
"DOMAIN",
|
|
|
|
"RP_ID",
|
|
|
|
"TOKEN_KEY",
|
|
|
|
"DATABASE_URL",
|
|
|
|
"ASSETS_DIR",
|
|
|
|
]
|
|
|
|
.into_iter()
|
|
|
|
.collect()
|
2024-08-06 11:08:15 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Environment {
|
2024-08-22 17:29:24 -04:00
|
|
|
hostname: String,
|
|
|
|
port: u32,
|
|
|
|
domain: String,
|
|
|
|
rp_id: String,
|
2024-08-06 11:08:15 -04:00
|
|
|
token_key: SymmetricKey<V4>,
|
|
|
|
database_url: String,
|
|
|
|
email_sender: Sender<UserConfirmationMessage>,
|
|
|
|
assets_dir: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Environment {
|
|
|
|
pub fn init(email_sender: Sender<UserConfirmationMessage>) -> Result<Self, AppError> {
|
|
|
|
let mut builder = EnvironmentObjectBuilder::new(email_sender);
|
|
|
|
dotenvy::dotenv_iter()
|
|
|
|
.expect("Missing .env file")
|
|
|
|
.filter_map(|item| item.ok())
|
2024-08-24 13:22:51 -04:00
|
|
|
.filter(|(key, _)| REQUIRED_ENV_VARS.contains(key.as_str()))
|
2024-08-06 11:08:15 -04:00
|
|
|
.for_each(|(key, value)| match key.as_str() {
|
2024-08-22 17:29:24 -04:00
|
|
|
"HOSTNAME" => builder.with_hostname(value),
|
|
|
|
"PORT" => builder.with_port(value),
|
2024-08-24 13:22:51 -04:00
|
|
|
"DOMAIN" => builder.with_domain(value),
|
|
|
|
"RP_ID" => builder.with_rp_id(value),
|
2024-08-06 11:08:15 -04:00
|
|
|
"TOKEN_KEY" => builder.with_token_key(value),
|
|
|
|
"DATABASE_URL" => builder.with_database_url(value),
|
|
|
|
"ASSETS_DIR" => builder.with_assets_dir(value),
|
|
|
|
_ => {}
|
|
|
|
});
|
|
|
|
|
2024-08-22 17:29:24 -04:00
|
|
|
let missing_vars = builder.uninitialized_variables();
|
|
|
|
if let Some(missing_vars) = missing_vars {
|
2024-08-06 11:08:15 -04:00
|
|
|
Err(AppError::missing_environment_variables(missing_vars))
|
|
|
|
} else {
|
|
|
|
Ok(Environment::from(builder))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-22 17:29:24 -04:00
|
|
|
pub fn hostname(&self) -> &str {
|
|
|
|
self.hostname.as_str()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn port(&self) -> u32 {
|
|
|
|
self.port
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn domain(&self) -> &str {
|
|
|
|
self.domain.as_str()
|
|
|
|
}
|
|
|
|
|
2024-08-24 13:22:51 -04:00
|
|
|
pub fn rp_id(&self) -> &str {
|
|
|
|
self.rp_id.as_str()
|
|
|
|
}
|
|
|
|
|
2024-08-06 11:08:15 -04:00
|
|
|
pub fn token_key(&self) -> &SymmetricKey<V4> {
|
|
|
|
&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<UserConfirmationMessage> {
|
|
|
|
&self.email_sender
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<EnvironmentObjectBuilder> for Environment {
|
|
|
|
fn from(builder: EnvironmentObjectBuilder) -> Self {
|
|
|
|
let EnvironmentObjectBuilder {
|
2024-08-22 17:29:24 -04:00
|
|
|
hostname,
|
|
|
|
port,
|
|
|
|
domain,
|
|
|
|
rp_id,
|
2024-08-06 11:08:15 -04:00
|
|
|
token_key,
|
|
|
|
database_url,
|
|
|
|
email_sender,
|
|
|
|
assets_dir,
|
|
|
|
} = builder;
|
|
|
|
|
|
|
|
Self {
|
2024-08-22 17:29:24 -04:00
|
|
|
hostname: hostname.unwrap(),
|
|
|
|
port: port.unwrap(),
|
|
|
|
domain: domain.unwrap(),
|
|
|
|
rp_id: rp_id.unwrap(),
|
2024-08-06 11:08:15 -04:00
|
|
|
token_key: token_key.unwrap(),
|
|
|
|
database_url: database_url.unwrap(),
|
|
|
|
email_sender: email_sender.unwrap(),
|
|
|
|
assets_dir: assets_dir.unwrap(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-24 13:22:51 -04:00
|
|
|
#[derive(Debug, Default)]
|
2024-08-06 11:08:15 -04:00
|
|
|
pub struct EnvironmentObjectBuilder {
|
2024-08-22 17:29:24 -04:00
|
|
|
pub hostname: Option<String>,
|
|
|
|
pub port: Option<u32>,
|
|
|
|
pub domain: Option<String>,
|
|
|
|
pub rp_id: Option<String>,
|
2024-08-06 11:08:15 -04:00
|
|
|
pub token_key: Option<SymmetricKey<V4>>,
|
|
|
|
pub database_url: Option<String>,
|
|
|
|
pub email_sender: Option<Sender<UserConfirmationMessage>>,
|
|
|
|
pub assets_dir: Option<PathBuf>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EnvironmentObjectBuilder {
|
|
|
|
pub fn new(email_sender: Sender<UserConfirmationMessage>) -> Self {
|
|
|
|
Self {
|
|
|
|
email_sender: Some(email_sender),
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn uninitialized_variables(&self) -> Option<Vec<&'static str>> {
|
2024-08-22 17:29:24 -04:00
|
|
|
let mut missing_vars = [
|
|
|
|
("HOSTNAME", self.hostname.as_deref()),
|
|
|
|
("DOMAIN", self.domain.as_deref()),
|
|
|
|
("RP_ID", self.rp_id.as_deref()),
|
|
|
|
("DATABASE_URL", self.database_url.as_deref()),
|
|
|
|
]
|
|
|
|
.into_iter()
|
2024-08-24 13:22:51 -04:00
|
|
|
.filter_map(|(key, value)| value.map(|_| key).xor(Some(key)))
|
2024-08-22 17:29:24 -04:00
|
|
|
.collect::<Vec<&'static str>>();
|
|
|
|
|
2024-08-06 11:08:15 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-22 17:29:24 -04:00
|
|
|
pub fn with_hostname(&mut self, hostname: String) {
|
|
|
|
self.hostname = Some(hostname);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_port(&mut self, port: String) {
|
|
|
|
let port = port
|
|
|
|
.parse::<u32>()
|
|
|
|
.inspect_err(|err| eprintln!("Not a valid port, defaulting to '42069': {err}"))
|
|
|
|
.ok();
|
|
|
|
|
|
|
|
self.port = port;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_domain(&mut self, domain: String) {
|
|
|
|
self.domain = Some(domain);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_rp_id(&mut self, rp_id: String) {
|
|
|
|
self.rp_id = Some(rp_id);
|
|
|
|
}
|
|
|
|
|
2024-08-06 11:08:15 -04:00
|
|
|
pub fn with_token_key(&mut self, key: String) {
|
|
|
|
match SymmetricKey::<V4>::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);
|
|
|
|
}
|
|
|
|
}
|