Merge branch 'field_config'
This commit is contained in:
commit
f36f8c7a7c
13 changed files with 316 additions and 123 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
# Dev
|
# Dev
|
||||||
config.yaml
|
config.yaml
|
||||||
|
log.txt
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
/node_modules/
|
/node_modules/
|
||||||
|
|
49
Cargo.lock
generated
49
Cargo.lock
generated
|
@ -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]]
|
||||||
|
|
|
@ -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
6
bacon.toml
Normal 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"]
|
117
src/config.rs
117
src/config.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
64
src/forms.rs
64
src/forms.rs
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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: ¶ms.config.lang,
|
lang: ¶ms.config.lang,
|
||||||
path_prefix: ¶ms.config.path_prefix,
|
path_prefix: ¶ms.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(¶ms.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: ¶ms.config.strings,
|
strings: ¶ms.config.strings,
|
||||||
|
custom_fields: ¶ms.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.
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
<div class="mb-3">
|
{% for custom_field in custom_fields %}
|
||||||
|
<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 %}
|
||||||
|
|
Loading…
Reference in a new issue