diff --git a/Containerfile b/Containerfile index e67b483..a53d3c7 100644 --- a/Containerfile +++ b/Containerfile @@ -14,7 +14,7 @@ RUN cargo build --release --bin contact-form FROM docker.io/library/debian:stable-slim AS runtime WORKDIR app -ENV CF_CONFIG_FILE=/volumes/data/config.yaml +ENV CF_DATA_DIR=/volumes/data COPY --from=builder /app/target/release/contact-form /usr/local/bin/contact-form COPY --from=builder /app/static/ ./static/ CMD ["contact-form"] diff --git a/src/config.rs b/src/config.rs index e208b9f..687b538 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use serde::Deserialize; -use std::{env, fs::File, io::BufReader}; +use std::{fs::File, io::BufReader, path::Path}; /// Email server credentials. #[derive(Deserialize)] @@ -20,7 +20,9 @@ pub struct Email { /// UTC offset for time formatting. #[derive(Deserialize)] pub struct UtcOffset { + #[serde(default)] pub hours: i8, + #[serde(default)] pub minutes: i8, } @@ -40,10 +42,18 @@ pub struct CustomField { pub field_type: CustomFieldType, } +fn default_captcha_error() -> String { + "You did enter the wrong code at the end of the form. Please try again.".to_string() +} +fn default_email_error() -> String { + "An internal error occurred while sending your request. Please try again later.".to_string() +} /// Error messages for localization. #[derive(Deserialize)] pub struct ErrorMessages { + #[serde(default = "default_captcha_error")] pub captcha_error: String, + #[serde(default = "default_email_error")] pub email_error: String, } @@ -79,7 +89,7 @@ fn default_captcha_label() -> String { "Enter the code above".to_string() } fn default_captcha_required_feedback() -> String { - "Please enter code from the image above".to_string() + "Please enter the code from the image above".to_string() } #[derive(Deserialize)] pub struct CaptchaField { @@ -89,14 +99,35 @@ pub struct CaptchaField { pub required_feedback: String, } +fn default_title() -> String { + "Contact form".to_string() +} +fn default_optional() -> String { + "optional".to_string() +} +fn default_submit() -> String { + "Submit".to_string() +} +fn default_success() -> String { + "Your request has been successfully submitted. We will get back to you soon.".to_string() +} +fn default_message_from() -> String { + "Message from".to_string() +} /// Localization strings. #[derive(Deserialize)] pub struct Strings { + #[serde(default)] pub description: String, + #[serde(default = "default_title")] pub title: String, + #[serde(default = "default_optional")] pub optional: String, + #[serde(default = "default_submit")] pub submit: String, + #[serde(default = "default_success")] pub success: String, + #[serde(default = "default_message_from")] pub message_from: String, pub name_field: NameField, pub email_field: EmailField, @@ -104,13 +135,16 @@ pub struct Strings { pub error_messages: ErrorMessages, } +fn default_path_prefix() -> String { + "/".to_string() +} fn default_lang() -> String { "en".to_string() } - #[derive(Deserialize)] pub struct StateConfig { /// The path prefix of all routes. + #[serde(default = "default_path_prefix")] pub path_prefix: String, pub custom_fields: Vec, /// The language tag of the HTML file. @@ -119,29 +153,35 @@ pub struct StateConfig { pub strings: Strings, } +fn default_socket_address() -> String { + "0.0.0.0:80".to_string() +} /// Configuration. #[derive(Deserialize)] pub struct Config { /// The server socket address including port. + #[serde(default = "default_socket_address")] pub socket_address: String, pub email: Email, - pub log_file: String, pub utc_offset: UtcOffset, #[serde(flatten)] pub state_config: StateConfig, } impl Config { - /// Parses the configuration from the config path in the environment variable. - pub fn build() -> Result { - // The environment variable with the path to the config file. - let config_file_var = "CF_CONFIG_FILE"; - let config_path = env::var(config_file_var) - .with_context(|| format!("Environment variable {config_file_var} missing!"))?; + /// Parses the configuration in the given data directory. + pub fn build(data_dir: &Path) -> Result { + let reader = { + let mut buf = data_dir.to_path_buf(); + buf.push("config.yaml"); + + let file = File::open(&buf).with_context(|| { + format!("Can not open the config file at the path {}", buf.display()) + })?; + + BufReader::new(file) + }; - let file = File::open(&config_path) - .with_context(|| format!("Can not open the config file at the path {config_path}"))?; - let reader = BufReader::new(file); let mut config: Self = serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?; diff --git a/src/logging.rs b/src/logging.rs index b98d464..d05c77b 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use std::fs::OpenOptions; +use std::{fs::OpenOptions, path::Path}; use time::{format_description::well_known::Rfc3339, UtcOffset}; use tracing_subscriber::{ filter::LevelFilter, @@ -9,11 +9,13 @@ use tracing_subscriber::{ Layer, }; +use crate::config; + /// Initializes the logger. -pub fn init_logger(log_file: &str, utc_offset_hours: i8, utc_offset_minutes: i8) -> Result<()> { +pub fn init_logger(data_dir: &Path, utc_offset: &config::UtcOffset) -> Result<()> { // Set UTC offset for time formatting. let timer = OffsetTime::new( - UtcOffset::from_hms(utc_offset_hours, utc_offset_minutes, 0) + UtcOffset::from_hms(utc_offset.hours, utc_offset.minutes, 0) .context("Failed to set the time offset from the given utc_hours_offset!")?, Rfc3339, ); @@ -31,11 +33,16 @@ pub fn init_logger(log_file: &str, utc_offset_hours: i8, utc_offset_minutes: i8) .with_filter(stdout_level_filter); // Log file. - let log_file = OpenOptions::new() - .create(true) - .append(true) - .open(log_file) - .context("Failed to open the log file in append mode!")?; + let log_file = { + let mut buf = data_dir.to_path_buf(); + buf.push("log.txt"); + + OpenOptions::new() + .create(true) + .append(true) + .open(buf) + .context("Failed to open the log file in append mode!")? + }; let file_layer = fmt::layer() .with_writer(log_file) .with_ansi(false) diff --git a/src/main.rs b/src/main.rs index 7398113..ad1f164 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,20 +13,23 @@ use axum::{ routing::{get, get_service, Router}, Server, }; -use std::{net::SocketAddr, process}; +use std::{env, net::SocketAddr, path::PathBuf, process}; use tower_http::services::ServeDir; use tracing::{error, info}; use crate::{config::Config, states::AppState}; -async fn init(logger_initialized: &mut bool) -> Result<()> { - let config = Config::build()?; +const DATA_DIR_ENV_VAR: &str = "CF_DATA_DIR"; - logging::init_logger( - &config.log_file, - config.utc_offset.hours, - config.utc_offset.minutes, - )?; +async fn init(logger_initialized: &mut bool) -> Result<()> { + let data_dir = PathBuf::from( + env::var(DATA_DIR_ENV_VAR) + .with_context(|| format!("Environment variable {DATA_DIR_ENV_VAR} missing!"))?, + ); + + let config = Config::build(&data_dir)?; + + logging::init_logger(&data_dir, &config.utc_offset)?; *logger_initialized = true; // The path prefix of all routes.