Merge branch 'field_config'

This commit is contained in:
Mo 2023-02-25 21:34:00 +01:00
commit f36f8c7a7c
13 changed files with 316 additions and 123 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
# Dev # Dev
config.yaml config.yaml
log.txt
# npm # npm
/node_modules/ /node_modules/

49
Cargo.lock generated
View file

@ -28,7 +28,7 @@ checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]] [[package]]
name = "askama" name = "askama"
version = "0.11.2" version = "0.11.2"
source = "git+https://github.com/djc/askama.git#70c5784a9ebc1e2f9e97d5358c7b686111ea18f4" source = "git+https://github.com/djc/askama.git#c9613ff6029ee58dc3f94c3dd955b905ed7fc9ef"
dependencies = [ dependencies = [
"askama_derive", "askama_derive",
"askama_escape", "askama_escape",
@ -40,7 +40,7 @@ dependencies = [
[[package]] [[package]]
name = "askama_axum" name = "askama_axum"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/djc/askama.git#70c5784a9ebc1e2f9e97d5358c7b686111ea18f4" source = "git+https://github.com/djc/askama.git#c9613ff6029ee58dc3f94c3dd955b905ed7fc9ef"
dependencies = [ dependencies = [
"askama", "askama",
"axum-core", "axum-core",
@ -50,7 +50,7 @@ dependencies = [
[[package]] [[package]]
name = "askama_derive" name = "askama_derive"
version = "0.12.0" version = "0.12.0"
source = "git+https://github.com/djc/askama.git#70c5784a9ebc1e2f9e97d5358c7b686111ea18f4" source = "git+https://github.com/djc/askama.git#c9613ff6029ee58dc3f94c3dd955b905ed7fc9ef"
dependencies = [ dependencies = [
"basic-toml", "basic-toml",
"mime", "mime",
@ -65,7 +65,7 @@ dependencies = [
[[package]] [[package]]
name = "askama_escape" name = "askama_escape"
version = "0.10.3" version = "0.10.3"
source = "git+https://github.com/djc/askama.git#70c5784a9ebc1e2f9e97d5358c7b686111ea18f4" source = "git+https://github.com/djc/askama.git#c9613ff6029ee58dc3f94c3dd955b905ed7fc9ef"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
@ -86,9 +86,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.6.7" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fb79c228270dcf2426e74864cabc94babb5dbab01a4314e702d2f16540e1591" checksum = "2bd379e511536bad07447f899300aa526e9bae8e6f66dc5e5ca45d7587b7c1ec"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
@ -111,7 +111,7 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http 0.3.5",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
] ]
@ -241,7 +241,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"time", "time",
"tokio", "tokio",
"tower-http", "tower-http 0.4.0",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -995,9 +995,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.108" version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e159d99e6c2b93995d171050271edb50ecc5288fbc7cc17de8fdce4e58c14" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1022,9 +1022,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
dependencies = [ dependencies = [
"itoa", "itoa",
"serde", "serde",
@ -1040,9 +1040,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]
@ -1135,6 +1135,25 @@ name = "tower-http"
version = "0.3.5" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
@ -1150,9 +1169,9 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]

View file

@ -17,6 +17,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9" serde_yaml = "0.9"
time = "0.3" time = "0.3"
tokio = { version = "1.25", default-features = false, features = ["macros", "rt"] } tokio = { version = "1.25", default-features = false, features = ["macros", "rt"] }
tower-http = { version = "0.3", features = ["fs"] } tower-http = { version = "0.4", features = ["fs"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["time"] } tracing-subscriber = { version = "0.3", features = ["time"] }

6
bacon.toml Normal file
View file

@ -0,0 +1,6 @@
default_job = "clippy"
[jobs.clippy]
command = ["cargo", "clippy", "--all-targets", "--color", "always", "--workspace"]
need_stdout = false
watch = ["tests", "benches", "examples", "crates", "templates"]

View file

@ -4,12 +4,19 @@ use std::{env, fs::File, io::BufReader};
/// Email server credentials. /// Email server credentials.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct EmailServer { pub struct EmailCredentials {
pub server_name: String, pub domain: String,
pub email: String, pub username: String,
pub password: String, pub password: String,
} }
#[derive(Deserialize)]
pub struct Email {
pub from: String,
pub to: String,
pub credentials: EmailCredentials,
}
/// UTC offset for time formatting. /// UTC offset for time formatting.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UtcOffset { pub struct UtcOffset {
@ -17,6 +24,22 @@ pub struct UtcOffset {
pub minutes: i8, pub minutes: i8,
} }
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum CustomFieldType {
Text,
Textarea { rows: u8 },
}
#[derive(Deserialize)]
pub struct CustomField {
pub key: String,
pub label: String,
#[serde(default)]
pub required_feedback: Option<String>,
pub field_type: CustomFieldType,
}
/// Error messages for localization. /// Error messages for localization.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ErrorMessages { pub struct ErrorMessages {
@ -24,11 +47,46 @@ pub struct ErrorMessages {
pub email_error: String, pub email_error: String,
} }
/// Field localization strings in a form with label and invalid feedback on wrong field input. fn default_name_label() -> String {
"Name".to_string()
}
fn default_name_required_feedback() -> String {
"Please enter your name".to_string()
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Field { pub struct NameField {
#[serde(default = "default_name_label")]
pub label: String, pub label: String,
pub invalid_feedback: String, #[serde(default = "default_name_required_feedback")]
pub required_feedback: String,
}
fn default_email_label() -> String {
"Email".to_string()
}
fn default_email_required_feedback() -> String {
"Please enter your email".to_string()
}
#[derive(Deserialize)]
pub struct EmailField {
#[serde(default = "default_email_label")]
pub label: String,
#[serde(default = "default_email_required_feedback")]
pub required_feedback: String,
}
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()
}
#[derive(Deserialize)]
pub struct CaptchaField {
#[serde(default = "default_captcha_label")]
pub label: String,
#[serde(default = "default_captcha_required_feedback")]
pub required_feedback: String,
} }
/// Localization strings. /// Localization strings.
@ -36,34 +94,41 @@ pub struct Field {
pub struct Strings { pub struct Strings {
pub description: String, pub description: String,
pub title: String, pub title: String,
pub name_field: Field, pub optional: String,
pub email_field: Field,
/// No invalid feedback because it is optional.
pub telefon_field_label: String,
pub message_field: Field,
pub captcha_field: Field,
pub submit: String, pub submit: String,
pub success: String, pub success: String,
pub message_from: String,
pub name_field: NameField,
pub email_field: EmailField,
pub captcha_field: CaptchaField,
pub error_messages: ErrorMessages,
}
fn default_lang() -> String {
"en".to_string()
}
#[derive(Deserialize)]
pub struct StateConfig {
/// The path prefix of all routes.
pub path_prefix: String,
pub custom_fields: Vec<CustomField>,
/// The language tag of the HTML file.
#[serde(default = "default_lang")]
pub lang: String,
pub strings: Strings,
} }
/// Configuration. /// Configuration.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Config { pub struct Config {
/// The language tag of the HTML file.
pub lang: String,
/// The path prefix of all routes.
pub path_prefix: String,
/// The server socket address including port. /// The server socket address including port.
pub socket_address: String, pub socket_address: String,
pub email_server: EmailServer, pub email: Email,
/// From mailbox.
pub email_from: String,
/// To mailbox.
pub email_to: String,
pub log_file: String, pub log_file: String,
pub utc_offset: UtcOffset, pub utc_offset: UtcOffset,
pub error_messages: ErrorMessages, #[serde(flatten)]
pub strings: Strings, pub state_config: StateConfig,
} }
impl Config { impl Config {
@ -77,9 +142,13 @@ impl Config {
let file = File::open(&config_path) let file = File::open(&config_path)
.with_context(|| format!("Can not open the config file at the path {config_path}"))?; .with_context(|| format!("Can not open the config file at the path {config_path}"))?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
let config = let mut config: Self =
serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?; serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?;
// Add a space at the end for the email subject.
config.state_config.strings.message_from =
config.state_config.strings.message_from.trim().to_string() + " ";
Ok(config) Ok(config)
} }
} }

View file

@ -12,7 +12,7 @@ impl IntoResponse for AppError {
// Log error. // Log error.
error!("{:?}", self.0); error!("{:?}", self.0);
StatusCode::BAD_REQUEST.into_response() (StatusCode::BAD_REQUEST, self.0.to_string()).into_response()
} }
} }

View file

@ -1,13 +1,26 @@
use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use crate::config::CustomField;
/// Fields of the contact form that persist after a redirection /// Fields of the contact form that persist after a redirection
/// (example after failed server side validation). /// (example after failed server side validation).
#[derive(Deserialize, Default)] #[derive(Deserialize)]
pub struct PersistantContactFormFields { pub struct PersistantFieldContents {
pub name: String, pub name: String,
pub email: String, pub email: String,
pub telefon: String, pub custom: Vec<String>,
pub message: String, }
impl PersistantFieldContents {
pub fn new_empty(custom_fields: &[CustomField]) -> Self {
Self {
name: String::default(),
email: String::default(),
custom: custom_fields.iter().map(|_| String::default()).collect(),
}
}
} }
/// The contact form. /// The contact form.
@ -16,6 +29,47 @@ pub struct ContactForm {
/// The id for the captcha. /// The id for the captcha.
pub id: u16, pub id: u16,
#[serde(flatten)] #[serde(flatten)]
pub persistant_fields: PersistantContactFormFields, pub persistant_field_contents: PersistantFieldContents,
pub captcha_answer: String, pub captcha_answer: String,
} }
impl ContactForm {
/// Build the form from the form map.
pub fn from_map(
mut map: HashMap<String, String>,
custom_fields: &Vec<CustomField>,
) -> Result<Self> {
let id = map
.get("id")
.context("id missing in the submitted form!")?
.parse()
.context("Failed to parse the submitted id!")?;
// Remove an item with the given key from the map.
let mut remove = |key| {
map.remove(key)
.with_context(|| format!("{} missing in the submitted form!", key))
};
// Default fields.
let captcha_answer = remove("captcha_answer")?;
let name = remove("name")?;
let email = remove("email")?;
// Custom fields.
let mut custom = Vec::with_capacity(custom_fields.len());
for field in custom_fields.iter() {
custom.push(remove(&field.key)?);
}
Ok(Self {
id,
persistant_field_contents: PersistantFieldContents {
name,
email,
custom,
},
captcha_answer,
})
}
}

View file

@ -7,8 +7,12 @@ use lettre::{
}, },
Tokio1Executor, Tokio1Executor,
}; };
use tracing::info;
use crate::config::Config; use crate::{
config::{Config, StateConfig},
forms::PersistantFieldContents,
};
type ASmtpTransport = AsyncSmtpTransport<Tokio1Executor>; type ASmtpTransport = AsyncSmtpTransport<Tokio1Executor>;
@ -23,12 +27,12 @@ impl Mailer {
pub async fn build(config: &Config) -> Result<Self> { pub async fn build(config: &Config) -> Result<Self> {
// Mail server credentials for login. // Mail server credentials for login.
let credentials = Credentials::new( let credentials = Credentials::new(
config.email_server.email.clone(), config.email.credentials.username.clone(),
config.email_server.password.clone(), config.email.credentials.password.clone(),
); );
// Establish the connection. // Establish the connection.
let mailer = ASmtpTransport::relay(&config.email_server.server_name) let mailer = ASmtpTransport::relay(&config.email.credentials.domain)
.context("Failed to connect to the email server!")? .context("Failed to connect to the email server!")?
.credentials(credentials) .credentials(credentials)
.build(); .build();
@ -36,17 +40,20 @@ impl Mailer {
.test_connection() .test_connection()
.await .await
.context("Email connection test failed!")?; .context("Email connection test failed!")?;
info!("Successful email connection!");
// Set the From and To mailboxes for every sent mail. // Set the From and To mailboxes for every sent mail.
let message_builder = Message::builder() let message_builder = Message::builder()
.from( .from(
config config
.email_from .email
.from
.parse() .parse()
.context("Failed to parse the From mailbox!")?, .context("Failed to parse the From mailbox!")?,
) )
.to(config .to(config
.email_to .email
.to
.parse() .parse()
.context("Failed to parse the To mailbox!")?); .context("Failed to parse the To mailbox!")?);
@ -57,23 +64,40 @@ impl Mailer {
} }
/// Sends a mail with data from the contact form. /// Sends a mail with data from the contact form.
pub async fn send(&self, name: &str, email: &str, telefon: &str, message: &str) -> Result<()> { pub async fn send(
let name = name.trim().to_string(); &self,
let subject = "Message from ".to_string() + &name; persistant_field_contents: &PersistantFieldContents,
config: &StateConfig,
) -> Result<()> {
let name = persistant_field_contents.name.trim().to_string();
let subject = config.strings.message_from.clone() + &name;
let reply_to = Mailbox::new( let reply_to = Mailbox::new(
Some(name), Some(name),
email persistant_field_contents
.email
.parse() .parse()
.context("Failed to parse the email from the form to an address!")?, .context("Failed to parse the email from the form to an address!")?,
); );
let telefon = telefon.trim(); let default_fields_content = format!(
let message = message.trim().to_string(); "{}: {}\n\n{}: {}\n\n\n",
let body = if !telefon.is_empty() { config.strings.name_field.label,
message + "\n\nTelefon: " + telefon persistant_field_contents.name,
} else { config.strings.email_field.label,
message persistant_field_contents.email
}; );
let body = config
.custom_fields
.iter()
.map(|field| &field.label)
.zip(
persistant_field_contents
.custom
.iter()
.map(|content| content.trim()),
)
.map(|(label, content)| format!("{label}:\n{content}\n\n\n"))
.fold(default_fields_content, |acc, s| acc + &s);
let email = self let email = self
.message_builder .message_builder

View file

@ -10,11 +10,10 @@ mod templates;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use axum::{ use axum::{
http::StatusCode,
routing::{get, get_service, Router}, routing::{get, get_service, Router},
Server, Server,
}; };
use std::{future::ready, net::SocketAddr, process}; use std::{net::SocketAddr, process};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::{error, info}; use tracing::{error, info};
@ -31,7 +30,7 @@ async fn init(logger_initialized: &mut bool) -> Result<()> {
*logger_initialized = true; *logger_initialized = true;
// The path prefix of all routes. // The path prefix of all routes.
let path_prefix = config.path_prefix.clone(); let path_prefix = config.state_config.path_prefix.clone();
let socket_address = config let socket_address = config
.socket_address .socket_address
@ -41,8 +40,7 @@ async fn init(logger_initialized: &mut bool) -> Result<()> {
let app_state = AppState::build(config).await?; let app_state = AppState::build(config).await?;
// The service for serving the static files. // The service for serving the static files.
let static_service = let static_service = get_service(ServeDir::new("static"));
get_service(ServeDir::new("static")).handle_error(|_| ready(StatusCode::NOT_FOUND));
let routes = Router::new() let routes = Router::new()
.route("/", get(routes::index).post(routes::submit)) .route("/", get(routes::index).post(routes::submit))

View file

@ -4,23 +4,26 @@ use axum::{
extract::{Form, State}, extract::{Form, State},
response::Response, response::Response,
}; };
use std::sync::{Arc, Mutex}; use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
captcha_solutions::CaptchaSolutions, captcha_solutions::CaptchaSolutions,
config::Config, config::StateConfig,
errors::AppError, errors::AppError,
forms::{ContactForm, PersistantContactFormFields}, forms::{ContactForm, PersistantFieldContents},
mailer::Mailer, mailer::Mailer,
templates, templates,
}; };
/// Parameters to render the contact form. /// Parameters to render the contact form.
pub struct ContactFormParams<'a> { pub struct ContactFormParams<'a> {
config: Arc<Config>, config: Arc<StateConfig>,
captcha_solutions: Arc<Mutex<CaptchaSolutions>>, captcha_solutions: Arc<Mutex<CaptchaSolutions>>,
persistant_fields: Option<PersistantContactFormFields>, persistant_field_contents: Option<PersistantFieldContents>,
error_message: Option<&'a str>, error_message: Option<&'a str>,
} }
@ -40,13 +43,16 @@ pub fn render_contact_form(params: ContactFormParams<'_>) -> Result<Response, Ap
lang: &params.config.lang, lang: &params.config.lang,
path_prefix: &params.config.path_prefix, path_prefix: &params.config.path_prefix,
}, },
was_validated: params.persistant_fields.is_some(), was_validated: params.persistant_field_contents.is_some(),
id, id,
// Default is empty fields. // Default is empty fields.
persistant_fields: params.persistant_fields.unwrap_or_default(), persistant_field_contents: params
.persistant_field_contents
.unwrap_or_else(|| PersistantFieldContents::new_empty(&params.config.custom_fields)),
captcha: captcha_base64, captcha: captcha_base64,
error_message: params.error_message.unwrap_or_default(), error_message: params.error_message.unwrap_or_default(),
strings: &params.config.strings, strings: &params.config.strings,
custom_fields: &params.config.custom_fields,
}; };
Ok(template.into_response()) Ok(template.into_response())
@ -54,7 +60,7 @@ pub fn render_contact_form(params: ContactFormParams<'_>) -> Result<Response, Ap
/// Index handler. /// Index handler.
pub async fn index( pub async fn index(
State(config): State<Arc<Config>>, State(config): State<Arc<StateConfig>>,
State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>, State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
info!("Visited get(index)"); info!("Visited get(index)");
@ -62,18 +68,20 @@ pub async fn index(
render_contact_form(ContactFormParams { render_contact_form(ContactFormParams {
config, config,
captcha_solutions, captcha_solutions,
persistant_fields: None, persistant_field_contents: None,
error_message: None, error_message: None,
}) })
} }
/// Submit handler. /// Submit handler.
pub async fn submit( pub async fn submit(
State(config): State<Arc<Config>>, State(config): State<Arc<StateConfig>>,
State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>, State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>,
State(mailer): State<Arc<Mailer>>, State(mailer): State<Arc<Mailer>>,
Form(form): Form<ContactForm>, Form(map): Form<HashMap<String, String>>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
let form = ContactForm::from_map(map, &config.custom_fields)?;
let right_captcha_answer = captcha_solutions let right_captcha_answer = captcha_solutions
.lock() .lock()
.unwrap() .unwrap()
@ -85,27 +93,19 @@ pub async fn submit(
return render_contact_form(ContactFormParams { return render_contact_form(ContactFormParams {
config: Arc::clone(&config), config: Arc::clone(&config),
captcha_solutions, captcha_solutions,
persistant_fields: Some(form.persistant_fields), persistant_field_contents: Some(form.persistant_field_contents),
error_message: Some(&config.error_messages.captcha_error), error_message: Some(&config.strings.error_messages.captcha_error),
}); });
} }
if let Err(e) = mailer if let Err(e) = mailer.send(&form.persistant_field_contents, &config).await {
.send(
&form.persistant_fields.name,
&form.persistant_fields.email,
&form.persistant_fields.telefon,
&form.persistant_fields.message,
)
.await
{
error!("{e:?}"); error!("{e:?}");
return render_contact_form(ContactFormParams { return render_contact_form(ContactFormParams {
config: Arc::clone(&config), config: Arc::clone(&config),
captcha_solutions, captcha_solutions,
persistant_fields: Some(form.persistant_fields), persistant_field_contents: Some(form.persistant_field_contents),
error_message: Some(&config.error_messages.email_error), error_message: Some(&config.strings.error_messages.email_error),
}); });
} }
@ -113,7 +113,7 @@ pub async fn submit(
} }
/// Called on successful contact form submission. /// Called on successful contact form submission.
pub fn success(config: Arc<Config>) -> Result<Response, AppError> { pub fn success(config: Arc<StateConfig>) -> Result<Response, AppError> {
info!("Successful contact form submission"); info!("Successful contact form submission");
// Initialize template. // Initialize template.

View file

@ -2,12 +2,16 @@ use anyhow::Result;
use axum::extract::FromRef; use axum::extract::FromRef;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::{captcha_solutions::CaptchaSolutions, config::Config, mailer::Mailer}; use crate::{
captcha_solutions::CaptchaSolutions,
config::{Config, StateConfig},
mailer::Mailer,
};
/// The application state. /// The application state.
#[derive(Clone, FromRef)] #[derive(Clone, FromRef)]
pub struct AppState { pub struct AppState {
pub config: Arc<Config>, pub config: Arc<StateConfig>,
pub mailer: Arc<Mailer>, pub mailer: Arc<Mailer>,
pub captcha_solutions: Arc<Mutex<CaptchaSolutions>>, pub captcha_solutions: Arc<Mutex<CaptchaSolutions>>,
} }
@ -17,12 +21,12 @@ impl AppState {
pub async fn build(config: Config) -> Result<Self> { pub async fn build(config: Config) -> Result<Self> {
let mailer = Arc::new(Mailer::build(&config).await?); let mailer = Arc::new(Mailer::build(&config).await?);
let config = Arc::new(config); let state_config = Arc::new(config.state_config);
// Mutex for write access. // Mutex for write access.
let captcha_solutions = Arc::new(Mutex::new(CaptchaSolutions::default())); let captcha_solutions = Arc::new(Mutex::new(CaptchaSolutions::default()));
Ok(Self { Ok(Self {
config, config: state_config,
mailer, mailer,
captcha_solutions, captcha_solutions,
}) })

View file

@ -1,6 +1,9 @@
use askama::Template; use askama::Template;
use crate::{config, forms::PersistantContactFormFields}; use crate::{
config::{self, CustomField},
forms::PersistantFieldContents,
};
/// Base template. /// Base template.
pub struct Base<'a> { pub struct Base<'a> {
@ -15,10 +18,11 @@ pub struct ContactForm<'a> {
pub base: Base<'a>, pub base: Base<'a>,
pub was_validated: bool, pub was_validated: bool,
pub id: u16, pub id: u16,
pub persistant_fields: PersistantContactFormFields, pub persistant_field_contents: PersistantFieldContents,
pub captcha: String, pub captcha: String,
pub error_message: &'a str, pub error_message: &'a str,
pub strings: &'a config::Strings, pub strings: &'a config::Strings,
pub custom_fields: &'a Vec<CustomField>,
} }
/// Sucessful contact form submission template. /// Sucessful contact form submission template.

View file

@ -16,42 +16,56 @@
<label for="name" class="form-label">{{ strings.name_field.label }}</label> <label for="name" class="form-label">{{ strings.name_field.label }}</label>
<input type="text" <input type="text"
name="name" name="name"
value="{{ persistant_fields.name }}" value="{{ persistant_field_contents.name }}"
class="form-control" class="form-control"
id="exampleInputEmail1" id="exampleInputEmail1"
required> required>
<div class="invalid-feedback">{{ strings.name_field.invalid_feedback }}</div> <div class="invalid-feedback">{{ strings.name_field.required_feedback }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email" class="form-label">{{ strings.email_field.label }}</label> <label for="email" class="form-label">{{ strings.email_field.label }}</label>
<input type="email" <input type="email"
name="email" name="email"
value="{{ persistant_fields.email }}" value="{{ persistant_field_contents.email }}"
class="form-control" class="form-control"
id="email" id="email"
required> required>
<div class="invalid-feedback">{{ strings.email_field.invalid_feedback }}</div> <div class="invalid-feedback">{{ strings.email_field.required_feedback }}</div>
</div>
<div class="mb-3">
<label for="telefon" class="form-label">{{ strings.telefon_field_label }}</label>
<input type="text"
name="telefon"
value="{{ persistant_fields.telefon }}"
class="form-control"
id="telefon">
</div>
<div class="mb-5">
<label for="message" class="form-label">{{ strings.message_field.label }}</label>
<textarea name="message"
rows="5"
class="form-control"
id="message"
style="resize: none"
required>{{ persistant_fields.message }}</textarea>
<div class="invalid-feedback">{{ strings.message_field.invalid_feedback }}</div>
</div> </div>
{% for custom_field in custom_fields %}
<div class="mb-3"> <div class="mb-3">
{% let value = persistant_field_contents.custom[loop.index0] %}
{% let required = custom_field.required_feedback.is_some() %}
<label for="{{ custom_field.key }}" class="form-label">{{ custom_field.label }}</label>
{% match custom_field.field_type %}
{% when config::CustomFieldType::Text %}
<input type="text"
class="form-control"
name="{{ custom_field.key }}"
id="{{ custom_field.key }}"
value="{{ value }}"
{% if required %}required{% endif %}>
{% when config::CustomFieldType::Textarea with { rows } %}
<textarea rows="{{ rows }}"
style="resize: none"
class="form-control"
name="{{ custom_field.key }}"
id="{{ custom_field.key }}"
{% if required %}required{% endif %}>{{ value }}</textarea>
{% endmatch %}
{% match custom_field.required_feedback %}
{% when Some with (feedback) %}
<div class="invalid-feedback">{{ feedback }}</div>
{% when None %}
{% endmatch %}
</div>
{% endfor %}
<div class="mb-2 mt-4">
<img src="data:image/png;base64,{{ captcha }}"> <img src="data:image/png;base64,{{ captcha }}">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -61,7 +75,7 @@
class="form-control" class="form-control"
id="captcha_answer" id="captcha_answer"
required> required>
<div class="invalid-feedback">{{ strings.captcha_field.invalid_feedback }}</div> <div class="invalid-feedback">{{ strings.captcha_field.required_feedback }}</div>
</div> </div>
{% if error_message.len() > 0 %} {% if error_message.len() > 0 %}