diff --git a/.gitignore b/.gitignore index 449d40a..6f71c71 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser +# API specific ignores +api/debt-pirate.config.toml diff --git a/api/.env b/api/.env deleted file mode 100644 index 0752049..0000000 --- a/api/.env +++ /dev/null @@ -1,9 +0,0 @@ -ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets -CACHE_URL=redis://debt_pirate:H553jOui2734@192.168.122.251:6379 -DATABASE_URL=postgres://debt_pirate:HRURqlUmtjIy@192.168.122.251/debt_pirate -HOSTNAME=127.0.0.1 -MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy -PORT=42069 -RUST_LOG=debt_pirate=trace -SEND_VERIFICATION_EMAIL=true -TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q diff --git a/api/.env.template b/api/.env.template deleted file mode 100644 index 1065183..0000000 --- a/api/.env.template +++ /dev/null @@ -1,3 +0,0 @@ -TOKEN_KEY= -DB_CONNECTION_URI= -ASSETS_DIR= diff --git a/api/Cargo.toml b/api/Cargo.toml index 149f146..2e73892 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -5,15 +5,14 @@ edition = "2021" [dependencies] argon2 = "0.5" -axum = { version = "0.7", features = [ +axum = { version = "0.8", features = [ "macros", "multipart", "ws", ] } base64 = "0.22" -bb8-redis = "0.17" +bb8-redis = "0.20" blake3 = { version = "1.5", features = ["serde"] } -dotenvy = "0.15" futures = "0.3" http = "1.0" humantime = "2.1" @@ -29,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [ ] } num_cpus = "1.16" pasetors = "0.7" -redis = { version = "0.27", features = ["aio"] } +redis = { version = "0.28", features = ["aio"] } serde = { version = "1.0", features = ["derive", "rc", "std"] } serde_json = "1.0" serde_with = "3.9" @@ -42,6 +41,7 @@ sqlx = { version = "0.8", features = [ syslog-tracing = "0.3.1" time = { version = "0.3", features = ["formatting", "macros"] } tokio = { version = "1.35", features = ["full"] } +toml = "0.8" tower = "0.5" tower-http = { version = "0.6", features = ["full"] } tracing = "0.1.40" diff --git a/api/debt-pirate.config.example.toml b/api/debt-pirate.config.example.toml new file mode 100644 index 0000000..796ed28 --- /dev/null +++ b/api/debt-pirate.config.example.toml @@ -0,0 +1,23 @@ +hostname = "127.0.0.1" +port = "42069" +assets_dir = '' +log = "info" + +[secrets] +token_key = '' + +[database] +host = "" +port = "5432" +user = "debt_pirate" +password = "" +schema = "debt_pirate" + +[cache] +host = "192.168.122.76" +port = "6379" +user = "debt_pirate" +password = "" + +[mail] +send_verification_email = true diff --git a/api/src/main.rs b/api/src/main.rs index 295fcd8..8010fd0 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,7 +1,5 @@ use std::{process, sync::mpsc::channel}; -use models::Environment; - mod db; mod models; mod requests; @@ -17,29 +15,20 @@ use tracing::{error, info}; #[tokio::main] async fn main() { - let (tx, rx) = channel::(); - - let env = match Environment::init(tx) { - Ok(env) => env, + let config = match services::config::read_config() { + Ok(config) => config, Err(err) => { eprintln!("{err}"); process::exit(1); } }; - initialize_logger(&env); + initialize_logger(config.logging_directive()); info!("Initializing database connection pool..."); - let db_pool = create_db_connection_pool(env.db_connection_uri().to_string()).await; + let db_pool = create_db_connection_pool(config.db_connection_uri().to_string()).await; info!("Database connection pool created successfully."); - info!("Initializing cache service connection pool..."); - let cache_pool = create_cache_connection_pool(env.cache_url().to_string()) - .await - .inspect_err(|err| error!(?err)) - .unwrap(); - info!("Cache service connection pool created successfully."); - info!("Running database schema migrations..."); if let Err(err) = run_migrations(&db_pool).await { eprintln!("{err:?}"); @@ -47,11 +36,19 @@ async fn main() { } info!("Database schema migrations completed successfully."); + info!("Initializing cache service connection pool..."); + let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string()) + .await + .inspect_err(|err| error!(?err)) + .unwrap(); + info!("Cache service connection pool created successfully."); + info!("Starting email sender service..."); - start_emailer_service(Handle::current(), env.assets_dir(), rx); + let (tx, rx) = channel::(); + start_emailer_service(Handle::current(), config.assets_dir(), rx); info!("Email service started successfully."); - if let Err(err) = start_app(db_pool, cache_pool, env).await { + if let Err(err) = start_app(db_pool, cache_pool, config, tx).await { eprintln!("{err:?}"); process::exit(3); } diff --git a/api/src/models/environment.rs b/api/src/models/environment.rs deleted file mode 100644 index e79d7a8..0000000 --- a/api/src/models/environment.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::mpsc::Sender, -}; - -use pasetors::{keys::SymmetricKey, version4::V4}; -use tracing::trace; -use url::Url; - -use crate::services::UserConfirmationMessage; - -use super::AppError; - -#[derive(Clone)] -pub struct Environment { - assets_dir: PathBuf, - cache_url: Url, - database_url: Url, - 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), - "CACHE_URL" => builder.with_cache_url(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 assets_dir(&self) -> &Path { - self.assets_dir.as_path() - } - - pub fn cache_url(&self) -> &Url { - &self.cache_url - } - - pub fn db_connection_uri(&self) -> &Url { - &self.database_url - } - - pub fn email_sender(&self) -> &Sender { - &self.email_sender - } - - pub fn hostname(&self) -> &str { - self.hostname.as_str() - } - - pub fn port(&self) -> u32 { - self.port - } - - pub fn rust_log(&self) -> &str { - self.rust_log.as_str() - } - - pub fn send_verification_email(&self) -> bool { - self.send_verification_email - } - - pub fn token_key(&self) -> &SymmetricKey { - &self.token_key - } -} - -impl From for Environment { - fn from(builder: EnvironmentObjectBuilder) -> Self { - let EnvironmentObjectBuilder { - assets_dir, - cache_url, - database_url, - email_sender, - hostname, - port, - rust_log, - send_verification, - token_key, - } = builder; - - Self { - assets_dir: assets_dir.unwrap(), - cache_url: cache_url.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 cache_url: 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()), - ("RUST_LOG", self.rust_log.as_deref()), - ] - .into_iter() - .filter_map(|(key, value)| value.map(|_| key).xor(Some(key))) - .collect::>(); - - if self.cache_url.is_none() { - missing_vars.push("CACHE_URL"); - } - - if self.database_url.is_none() { - missing_vars.push("DATABASE_URL"); - } - - 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_cache_url(&mut self, url: String) { - trace!(?url); - - let cache_url = url - .parse::() - .expect("The 'CACHE_URL' variable is not in valid URI format"); - - trace!(?cache_url); - - if cache_url.scheme().to_lowercase() != "redis" { - panic!("The 'CACHE_URL' must be a valid Redis connection string; it must use the 'redis://' scheme"); - } - - self.cache_url = Some(cache_url); - } - - pub fn with_database_url(&mut self, url: String) { - let database_url = url - .parse::() - .expect("The 'DATABASE_URL' variable is not in valid URI format"); - - self.database_url = Some(database_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); - } -} diff --git a/api/src/models/error.rs b/api/src/models/error.rs index a2081dd..ef92e55 100644 --- a/api/src/models/error.rs +++ b/api/src/models/error.rs @@ -1,10 +1,16 @@ -use std::{borrow::Cow, error::Error, fmt::Display, io}; +use std::{ + borrow::Cow, + error::Error, + fmt::Display, + io::{self, Error as IoError}, +}; use axum::response::IntoResponse; use bb8_redis::bb8::RunError; use http::StatusCode; use redis::RedisError; use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError}; +use toml::{self, de::Error as TomlError}; use tracing::trace; use super::ApiResponse; @@ -49,8 +55,8 @@ impl AppError { Self::new(ErrorKind::MissingAuthorizationToken) } - pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self { - Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars)) + pub fn missing_config_file() -> Self { + Self::new(ErrorKind::MissingConfigFile) } pub fn missing_session_field(field: &'static str) -> Self { @@ -65,10 +71,6 @@ impl AppError { Self::new(ErrorKind::NoUserFound) } - pub fn token_key() -> Self { - Self::new(ErrorKind::TokenKey) - } - pub fn is_duplicate_record(&self) -> bool { matches!(self.kind, ErrorKind::DuplicateRecord(_)) } @@ -84,12 +86,32 @@ impl From for AppError { } } +impl From for AppError { + fn from(other: IoError) -> Self { + Self::new(ErrorKind::ConfigFile(other)) + } +} + impl From for AppError { fn from(other: MigrateError) -> Self { Self::new(ErrorKind::DbMigration(other)) } } +impl From for AppError { + fn from(other: RedisError) -> Self { + trace!(err = ?other, "Cache error"); + Self::new(ErrorKind::Cache(other.to_string())) + } +} + +impl From> for AppError { + fn from(other: RunError) -> Self { + trace!(err = ?other, "Cache pool error"); + Self::new(ErrorKind::Cache(other.to_string())) + } +} + impl From for AppError { fn from(other: SqlxError) -> Self { match &other { @@ -107,17 +129,9 @@ impl From for AppError { } } -impl From for AppError { - fn from(other: RedisError) -> Self { - trace!(err = ?other, "Cache error"); - Self::new(ErrorKind::Cache(other.to_string())) - } -} - -impl From> for AppError { - fn from(other: RunError) -> Self { - trace!(err = ?other, "Cache pool error"); - Self::new(ErrorKind::Cache(other.to_string())) +impl From for AppError { + fn from(other: TomlError) -> Self { + Self::new(ErrorKind::ConfigParse(other)) } } @@ -126,6 +140,10 @@ impl Display for AppError { match &self.kind { ErrorKind::AppStartupError(err) => write!(f, "{err}"), ErrorKind::Cache(err) => write!(f, "{err}"), + ErrorKind::ConfigFile(err) => write!(f, "Unable to application config file: {err}"), + ErrorKind::ConfigParse(err) => { + write!(f, "Unable to parse application config file: {err}") + } ErrorKind::ConnectionInfo(service) => write!( f, "Unable to connect to '{service}' service; invalid connection string" @@ -141,12 +159,8 @@ impl Display for AppError { ErrorKind::ExpiredToken => write!(f, "The provided token has expired"), ErrorKind::InvalidCredentials => write!(f, "Invalid email address or password"), ErrorKind::InvalidToken => write!(f, "The provided token is invalid"), + ErrorKind::MissingConfigFile => write!(f, "Application config file not found"), ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"), - ErrorKind::MissingEnvironmentVariables(missing_vars) => write!( - f, - "Missing required environment variables: {}", - missing_vars.join(", ") - ), ErrorKind::MissingSessionField(field) => write!( f, "Cannot retrieve session: missing required field: {field}" @@ -168,6 +182,8 @@ impl Display for AppError { enum ErrorKind { AppStartupError(io::Error), Cache(String), + ConfigFile(IoError), + ConfigParse(TomlError), ConnectionInfo(&'static str), Database, DbMigration(MigrateError), @@ -175,7 +191,7 @@ enum ErrorKind { ExpiredToken, InvalidCredentials, InvalidToken, - MissingEnvironmentVariables(Vec<&'static str>), + MissingConfigFile, MissingSessionField(&'static str), MissingAuthorizationToken, NoDbRecordFound, diff --git a/api/src/models/mod.rs b/api/src/models/mod.rs index df1ca68..08cb0f6 100644 --- a/api/src/models/mod.rs +++ b/api/src/models/mod.rs @@ -1,9 +1,7 @@ mod api_response; -mod environment; mod error; mod session; pub use api_response::*; -pub use environment::*; pub use error::*; pub use session::*; diff --git a/api/src/models/session.rs b/api/src/models/session.rs index 558d417..b182c99 100644 --- a/api/src/models/session.rs +++ b/api/src/models/session.rs @@ -1,6 +1,6 @@ use std::time::SystemTime; -use axum::{async_trait, extract::FromRequestParts}; +use axum::extract::FromRequestParts; use http::request::Parts; use humantime::format_rfc3339; use redis::ToRedisArgs; @@ -41,7 +41,6 @@ impl Session { } } -#[async_trait] impl FromRequestParts for Session { type Rejection = AppError; @@ -50,7 +49,7 @@ impl FromRequestParts for Session { state: &'b AppState, ) -> Result { let cache_pool = state.cache_pool(); - let token_key = state.env().token_key(); + let token_key = state.config().secrets().token_key(); let trusted_token_str = auth_token::extract_token_string_from_http_headers(&parts.headers)?; let trusted_token = verify_token(token_key, trusted_token_str, None)?; diff --git a/api/src/requests/auth/login/handler.rs b/api/src/requests/auth/login/handler.rs index 8fd6be1..de49b87 100644 --- a/api/src/requests/auth/login/handler.rs +++ b/api/src/requests/auth/login/handler.rs @@ -32,7 +32,7 @@ pub async fn auth_login_post_handler( ) -> Result { let db_pool = state.db_pool(); let cache_pool = state.cache_pool(); - let token_key = state.env().token_key(); + let token_key = state.config().secrets().token_key(); auth_login_request(db_pool, cache_pool, token_key, body).await } diff --git a/api/src/requests/auth/session/handler.rs b/api/src/requests/auth/session/handler.rs index 04f5111..ba15926 100644 --- a/api/src/requests/auth/session/handler.rs +++ b/api/src/requests/auth/session/handler.rs @@ -24,7 +24,7 @@ pub async fn auth_session_get_handler( headers: HeaderMap, ) -> Result { let cache_pool = state.cache_pool(); - let token_key = state.env().token_key(); + let token_key = state.config().secrets().token_key(); let auth_token_str = auth_token::extract_token_string_from_http_headers(&headers)?; let auth_token = verify_token(token_key, auth_token_str, None)?; diff --git a/api/src/requests/mod.rs b/api/src/requests/mod.rs index 02402d3..cc7436d 100644 --- a/api/src/requests/mod.rs +++ b/api/src/requests/mod.rs @@ -1,7 +1,7 @@ mod auth; mod user; -use std::time::Duration; +use std::{sync::mpsc::Sender, time::Duration}; use axum::{ extract::{MatchedPath, Request}, @@ -16,23 +16,30 @@ use uuid::Uuid; use crate::{ db::DbPool, - models::{AppError, Environment}, - services::CachePool, + models::AppError, + services::{config::Config, CachePool, UserConfirmationMessage}, }; #[derive(Clone)] pub struct AppState { db_pool: DbPool, cache_pool: CachePool, - env: Environment, + config: Config, + mail_sender: Sender, } impl AppState { - pub fn new(db_pool: DbPool, cache_pool: CachePool, env: Environment) -> Self { + pub fn new( + db_pool: DbPool, + cache_pool: CachePool, + config: Config, + mail_sender: Sender, + ) -> Self { Self { db_pool, cache_pool, - env, + config, + mail_sender, } } @@ -44,24 +51,25 @@ impl AppState { &self.cache_pool } - pub fn env(&self) -> &Environment { - &self.env + pub fn config(&self) -> &Config { + &self.config + } + + pub fn mail_sender(&self) -> &Sender { + &self.mail_sender } } pub async fn start_app( db_pool: DbPool, cache_pool: CachePool, - env: Environment, + config: Config, + mail_sender: Sender, ) -> Result<(), AppError> { - let address = env.hostname(); - let port = env.port(); - - info!("Listening on {address}:{port}..."); - let listener = TcpListener::bind(format!("{address}:{port}")) - .await - .unwrap(); + let connection_uri = config.app_connection_uri(); + info!("Listening on {connection_uri}..."); + let listener = TcpListener::bind(connection_uri).await.unwrap(); let logging_layer = TraceLayer::new_for_http() .make_span_with(|request: &Request| { let path = request @@ -82,7 +90,7 @@ pub async fn start_app( } }); - let state = AppState::new(db_pool, cache_pool, env); + let state = AppState::new(db_pool, cache_pool, config, mail_sender); let app = Router::new() .merge(user::requests(state.clone())) .merge(auth::requests(state.clone())) diff --git a/api/src/requests/user/create/handler.rs b/api/src/requests/user/create/handler.rs index 3866fd0..ad9cdab 100644 --- a/api/src/requests/user/create/handler.rs +++ b/api/src/requests/user/create/handler.rs @@ -26,15 +26,16 @@ pub async fn user_registration_post_handler( State(state): State, Json(request): Json, ) -> Result { - let env = state.env(); + let config = state.config(); + let mail_sender = state.mail_sender(); register_new_user_request( request, state.db_pool(), state.cache_pool(), - env.token_key(), - env.send_verification_email(), - env.email_sender(), + config.secrets().token_key(), + config.mail().send_verification_email, + mail_sender, ) .await } diff --git a/api/src/requests/user/verify/handler.rs b/api/src/requests/user/verify/handler.rs index 8c4428f..c42847f 100644 --- a/api/src/requests/user/verify/handler.rs +++ b/api/src/requests/user/verify/handler.rs @@ -31,10 +31,9 @@ pub async fn user_verification_get_handler( ) -> Result { let db_pool = state.db_pool(); let cache_pool = state.cache_pool(); - let env = state.env(); + let token_key = state.config().secrets().token_key(); let UserVerifyGetParams { verification_token } = query; - let token_key = env.token_key(); verify_new_user_request(db_pool, cache_pool, user_id, verification_token, token_key).await } diff --git a/api/src/services/config.rs b/api/src/services/config.rs new file mode 100644 index 0000000..4de7b48 --- /dev/null +++ b/api/src/services/config.rs @@ -0,0 +1,185 @@ +use std::{ + env, + fmt::Display, + fs, + path::{Path, PathBuf}, +}; + +use http::Uri; +use pasetors::{keys::SymmetricKey, version4::V4}; +use serde::{de::Error as DeserializerError, Deserialize, Deserializer}; + +use crate::models::AppError; + +pub fn read_config() -> Result { + let cwd_config_file = env::current_dir() + .map(|cwd| cwd.join("debt-pirate.config.toml")) + .unwrap(); + let base_override_config_file = PathBuf::from("/etc/debt-pirate/local.toml"); + let base_config_file = PathBuf::from("/etc/debt-pirate/config.toml"); + + let config_file_path = match ( + cwd_config_file.try_exists().unwrap_or_default(), + base_override_config_file.try_exists().unwrap_or_default(), + base_config_file.try_exists().unwrap_or_default(), + ) { + (true, _, _) => cwd_config_file.as_path(), + (false, true, _) => base_override_config_file.as_path(), + (false, false, true) => base_config_file.as_path(), + (false, false, false) => return Err(AppError::missing_config_file()), + }; + + Config::try_initialize_from_file(config_file_path) +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + hostname: String, + port: String, + assets_dir: PathBuf, + log: ConfigLogLevel, + database: ConfigDatabase, + cache: ConfigCache, + secrets: ConfigSecrets, + mail: ConfigMail, +} + +impl Config { + pub fn try_initialize_from_file(file: &Path) -> Result { + let buffer = fs::read_to_string(file)?; + toml::from_str(buffer.as_str()).map_err(AppError::from) + } + + pub fn app_connection_uri(&self) -> String { + format!("{}:{}", self.hostname.as_str(), self.port.as_str()) + } + + pub fn db_connection_uri(&self) -> Uri { + self.database.connection_uri() + } + + pub fn cache_connection_uri(&self) -> Uri { + self.cache.connection_uri() + } + + pub fn logging_directive(&self) -> String { + format!("{}={}", env!("CARGO_PKG_NAME"), self.log) + } + + pub fn assets_dir(&self) -> &Path { + self.assets_dir.as_path() + } + + pub fn secrets(&self) -> &ConfigSecrets { + &self.secrets + } + + pub fn mail(&self) -> &ConfigMail { + &self.mail + } +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(untagged)] +pub enum ConfigLogLevel { + #[serde(rename = "trace")] + Trace, + + #[serde(rename = "debug")] + Debug, + + #[serde(rename = "info")] + Info, + + #[serde(rename = "warn")] + Warn, + + #[serde(rename = "error")] + Error, +} + +impl Display for ConfigLogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match *self { + Self::Trace => "trace", + Self::Debug => "debug", + Self::Info => "info", + Self::Warn => "warn", + Self::Error => "error", + } + ) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ConfigDatabase { + host: String, + port: String, + user: String, + password: String, + schema: String, +} + +impl ConfigDatabase { + pub fn connection_uri(&self) -> Uri { + Uri::builder() + .scheme("postgres") + .authority(format!( + "{}:{}@{}:{}", + self.user, self.password, self.host, self.port + )) + .path_and_query(format!("/{}", self.schema)) + .build() + .unwrap() + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ConfigCache { + host: String, + port: String, + user: String, + password: String, +} + +impl ConfigCache { + pub fn connection_uri(&self) -> Uri { + Uri::builder() + .scheme("redis") + .authority(format!( + "{}:{}@{}:{}", + self.user, self.password, self.host, self.port + )) + .build() + .unwrap() + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ConfigSecrets { + #[serde(deserialize_with = "deserialize_symmetric_key")] + token_key: SymmetricKey, +} + +impl ConfigSecrets { + pub fn token_key(&self) -> &SymmetricKey { + &self.token_key + } +} + +fn deserialize_symmetric_key<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let buffer = String::deserialize(deserializer)?; + SymmetricKey::::try_from(buffer.as_str()) + .map_err(|_| DeserializerError::custom("Invalid PASETO v4 local key")) +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ConfigMail { + pub send_verification_email: bool, +} diff --git a/api/src/services/logger.rs b/api/src/services/logger.rs index ed70df3..22dc52c 100644 --- a/api/src/services/logger.rs +++ b/api/src/services/logger.rs @@ -3,7 +3,7 @@ use std::time::SystemTime; use humantime::format_rfc3339_millis; use time::macros::format_description; -use tracing::{level_filters::LevelFilter, Event, Subscriber}; +use tracing::{level_filters::LevelFilter, Event, Level, Subscriber}; use tracing_subscriber::{ fmt::{ format::{DefaultFields, Writer}, @@ -14,13 +14,12 @@ use tracing_subscriber::{ EnvFilter, }; -use crate::models::Environment; +use super::config::ConfigLogLevel; -pub fn initialize_logger(env: &Environment) { - let log_level = env.rust_log(); +pub fn initialize_logger(log_directive: String) { let env_filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) - .parse_lossy(log_level); + .parse_lossy(log_directive); #[cfg(debug_assertions)] tracing_subscriber::fmt() diff --git a/api/src/services/mod.rs b/api/src/services/mod.rs index 1b397a5..b98c283 100644 --- a/api/src/services/mod.rs +++ b/api/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod auth_token; mod cache; +pub mod config; mod logger; mod mailer; mod password_hasher;