Upgrade to Axum 0.6 and refactor
This commit is contained in:
parent
ef93f8a76a
commit
be7b29d631
12 changed files with 188 additions and 111 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,6 +1,11 @@
|
||||||
|
# Rust
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Dev
|
||||||
|
config.yaml
|
||||||
|
/logs
|
||||||
|
|
||||||
|
# npm
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
/target/
|
|
||||||
config.json
|
|
||||||
logs
|
|
||||||
|
|
6
.typos.toml
Normal file
6
.typos.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[files]
|
||||||
|
extend-exclude = [
|
||||||
|
"/static/",
|
||||||
|
"/config.yaml",
|
||||||
|
]
|
||||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -10,19 +10,13 @@ license-file = "LICENSE.txt"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
askama = { git = "https://github.com/djc/askama.git" }
|
askama = { git = "https://github.com/djc/askama.git" }
|
||||||
askama_axum = { git = "https://github.com/djc/askama.git", package = "askama_axum" }
|
askama_axum = { git = "https://github.com/djc/askama.git", package = "askama_axum" }
|
||||||
axum = { version = "0.5", default-features = false, features = [
|
axum = { version = "0.6", default-features = false, features = ["http1", "query", "form", "tokio", "macros"] }
|
||||||
"http1",
|
axum-extra = { version = "0.4", features = ["spa"] }
|
||||||
"query",
|
captcha = { version = "0.0.9", default-features = false }
|
||||||
"form",
|
lettre = { version = "0.10", default-features = false, features = ["smtp-transport", "hostname", "rustls-tls", "pool", "builder"] }
|
||||||
] }
|
|
||||||
axum-extra = { version = "0.3", features = ["spa"] }
|
|
||||||
captcha = "0.0.9"
|
|
||||||
lettre = "0.10"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_yaml = "0.9"
|
||||||
tokio = { version = "1.21", features = ["full"] }
|
tokio = { version = "1.22", features = ["full"] }
|
||||||
tower = { version = "0.4", features = ["limit", "buffer"] }
|
|
||||||
tower-http = { version = "0.3", features = ["trace"] }
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|
|
@ -9,8 +9,8 @@ pub struct SharedCaptchaSolutions {
|
||||||
arc: Arc<Mutex<CaptchaSolutions>>,
|
arc: Arc<Mutex<CaptchaSolutions>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedCaptchaSolutions {
|
impl Default for SharedCaptchaSolutions {
|
||||||
pub fn new() -> Self {
|
fn default() -> Self {
|
||||||
let max_size = (u16::MAX as usize) + 1;
|
let max_size = (u16::MAX as usize) + 1;
|
||||||
let mut solutions = Vec::with_capacity(max_size);
|
let mut solutions = Vec::with_capacity(max_size);
|
||||||
solutions.resize(max_size, None);
|
solutions.resize(max_size, None);
|
||||||
|
@ -22,7 +22,9 @@ impl SharedCaptchaSolutions {
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedCaptchaSolutions {
|
||||||
fn lock(&self) -> MutexGuard<CaptchaSolutions> {
|
fn lock(&self) -> MutexGuard<CaptchaSolutions> {
|
||||||
self.arc.lock().unwrap()
|
self.arc.lock().unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,32 @@ pub struct Address {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Logging {
|
pub struct Logging {
|
||||||
pub directory: String,
|
pub directory: String,
|
||||||
pub filename_prefix: String,
|
pub filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ErrorMessages {
|
||||||
|
pub captcha_error: String,
|
||||||
|
pub email_error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Field {
|
||||||
|
pub label: String,
|
||||||
|
pub invalid_feedback: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Strings {
|
||||||
|
pub description: String,
|
||||||
|
pub title: String,
|
||||||
|
pub name_field: Field,
|
||||||
|
pub email_field: Field,
|
||||||
|
pub telefon_field_label: String,
|
||||||
|
pub message_field: Field,
|
||||||
|
pub captcha_field: Field,
|
||||||
|
pub submit: String,
|
||||||
|
pub success: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -38,6 +63,8 @@ pub struct Config {
|
||||||
pub email_from: Address,
|
pub email_from: Address,
|
||||||
pub email_to: Address,
|
pub email_to: Address,
|
||||||
pub logging: Logging,
|
pub logging: Logging,
|
||||||
|
pub error_messages: ErrorMessages,
|
||||||
|
pub strings: Strings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -49,8 +76,8 @@ impl Config {
|
||||||
let config_file = File::open(&config_path)
|
let config_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 config_reader = BufReader::new(config_file);
|
let config_reader = BufReader::new(config_file);
|
||||||
let config: Self = serde_json::from_reader(config_reader)
|
let config: Self = serde_yaml::from_reader(config_reader)
|
||||||
.context("Can not parse the config file as JSON!")?;
|
.context("Can not parse the YAML config file!")?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
pub struct AppError(anyhow::Error);
|
pub struct AppError(anyhow::Error);
|
||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
|
error!("{:?}", self.0);
|
||||||
|
|
||||||
StatusCode::BAD_REQUEST.into_response()
|
StatusCode::BAD_REQUEST.into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,8 @@ use tracing_subscriber::filter::LevelFilter;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
|
||||||
pub fn init_logger(logging_config: &config::Logging) -> WorkerGuard {
|
pub fn init_logger(logging_config: &config::Logging) -> WorkerGuard {
|
||||||
let file_appender = tracing_appender::rolling::hourly(
|
let file_appender =
|
||||||
&logging_config.directory,
|
tracing_appender::rolling::never(&logging_config.directory, &logging_config.filename);
|
||||||
&logging_config.filename_prefix,
|
|
||||||
);
|
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -5,23 +5,17 @@ mod forms;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod mailer;
|
mod mailer;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod states;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::extract::Extension;
|
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{error_handling::HandleErrorLayer, http::StatusCode, BoxError};
|
|
||||||
use axum::{Router, Server};
|
use axum::{Router, Server};
|
||||||
use axum_extra::routing::SpaRouter;
|
use axum_extra::routing::SpaRouter;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use tracing::info;
|
||||||
use tower::buffer::BufferLayer;
|
|
||||||
use tower::limit::RateLimitLayer;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::trace::{DefaultOnResponse, TraceLayer};
|
|
||||||
use tracing::Level;
|
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
|
||||||
async fn init() -> Result<WorkerGuard> {
|
async fn init() -> Result<WorkerGuard> {
|
||||||
|
@ -40,34 +34,25 @@ async fn init() -> Result<WorkerGuard> {
|
||||||
let tracing_worker_gurad = logging::init_logger(&config.logging);
|
let tracing_worker_gurad = logging::init_logger(&config.logging);
|
||||||
|
|
||||||
let config = Arc::new(config);
|
let config = Arc::new(config);
|
||||||
let captcha_solutions = Arc::new(captcha_solutions::SharedCaptchaSolutions::new());
|
let captcha_solutions = Arc::new(captcha_solutions::SharedCaptchaSolutions::default());
|
||||||
|
|
||||||
|
let app_state = states::AppState {
|
||||||
|
config,
|
||||||
|
mailer,
|
||||||
|
captcha_solutions,
|
||||||
|
};
|
||||||
|
|
||||||
let spa = SpaRouter::new(&format!("{}/static", &path_prefix), "static");
|
let spa = SpaRouter::new(&format!("{}/static", &path_prefix), "static");
|
||||||
|
|
||||||
let routes = Router::new()
|
let routes = Router::new()
|
||||||
.route("/", get(routes::index))
|
.route("/", get(routes::index))
|
||||||
.route("/submit", post(routes::submit))
|
.route("/", post(routes::submit))
|
||||||
.route("/success", get(routes::success));
|
.route("/success", get(routes::success))
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new().nest(&path_prefix, routes).merge(spa);
|
||||||
.nest(&path_prefix, routes)
|
|
||||||
.merge(spa)
|
|
||||||
.layer(TraceLayer::new_for_http().on_response(DefaultOnResponse::new().level(Level::INFO)))
|
|
||||||
.layer(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(HandleErrorLayer::new(|err: BoxError| async move {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Unhandled error: {}", err),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.layer(BufferLayer::new(1024))
|
|
||||||
.layer(RateLimitLayer::new(2, Duration::from_secs(3))),
|
|
||||||
)
|
|
||||||
.layer(Extension(config))
|
|
||||||
.layer(Extension(mailer))
|
|
||||||
.layer(Extension(captcha_solutions));
|
|
||||||
|
|
||||||
|
info!("Starting server");
|
||||||
Server::bind(&socket_address)
|
Server::bind(&socket_address)
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
.await
|
.await
|
||||||
|
|
106
src/routes.rs
106
src/routes.rs
|
@ -1,92 +1,132 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use askama_axum::IntoResponse;
|
use askama_axum::IntoResponse;
|
||||||
use axum::extract::{Extension, Form, Query};
|
use axum::extract::{Form, State};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{captcha_solutions, config, errors, forms, mailer, templates};
|
use crate::{captcha_solutions, config, errors, forms, mailer, templates};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
pub struct IndexParams<'a> {
|
||||||
pub struct IndexParams {
|
config: Arc<config::Config>,
|
||||||
was_validated: Option<bool>,
|
captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>,
|
||||||
|
was_validated: bool,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
telefon: Option<String>,
|
telefon: Option<String>,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
|
error_message: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
Query(params): Query<IndexParams>,
|
State(config): State<Arc<config::Config>>,
|
||||||
config: Extension<Arc<config::Config>>,
|
State(captcha_solutions): State<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
||||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
|
||||||
) -> Result<Response, errors::AppError> {
|
) -> Result<Response, errors::AppError> {
|
||||||
|
info!("Visited get(index)");
|
||||||
|
render_contact_form(IndexParams {
|
||||||
|
config,
|
||||||
|
captcha_solutions,
|
||||||
|
was_validated: false,
|
||||||
|
name: None,
|
||||||
|
email: None,
|
||||||
|
telefon: None,
|
||||||
|
message: None,
|
||||||
|
error_message: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_contact_form(params: IndexParams<'_>) -> Result<Response, errors::AppError> {
|
||||||
let captcha = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy);
|
let captcha = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy);
|
||||||
let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?;
|
let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?;
|
||||||
|
|
||||||
let id = captcha_solutions.store_solution(&captcha.chars_as_string());
|
let id = params
|
||||||
|
.captcha_solutions
|
||||||
|
.store_solution(&captcha.chars_as_string());
|
||||||
|
|
||||||
let template = templates::ContactForm {
|
let template = templates::ContactForm {
|
||||||
path_prefix: &config.path_prefix,
|
path_prefix: ¶ms.config.path_prefix,
|
||||||
was_validated: params.was_validated.unwrap_or(false),
|
was_validated: params.was_validated,
|
||||||
id,
|
id,
|
||||||
name: ¶ms.name.unwrap_or_default(),
|
name: params.name.unwrap_or_default(),
|
||||||
email: ¶ms.email.unwrap_or_default(),
|
email: params.email.unwrap_or_default(),
|
||||||
telefon: ¶ms.telefon.unwrap_or_default(),
|
telefon: params.telefon.unwrap_or_default(),
|
||||||
message: ¶ms.message.unwrap_or_default(),
|
message: params.message.unwrap_or_default(),
|
||||||
captcha: &captcha_base64,
|
captcha: captcha_base64,
|
||||||
|
error_message: params.error_message.unwrap_or_default(),
|
||||||
|
strings: ¶ms.config.strings,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(template.into_response())
|
Ok(template.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn back_to_index(
|
async fn failed_submission(
|
||||||
|
config: Arc<config::Config>,
|
||||||
|
captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>,
|
||||||
|
error_message: &str,
|
||||||
mut form: forms::ContactForm,
|
mut form: forms::ContactForm,
|
||||||
config: Extension<Arc<config::Config>>,
|
|
||||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
|
||||||
) -> Result<Response, errors::AppError> {
|
) -> Result<Response, errors::AppError> {
|
||||||
let name = mem::take(&mut form.name);
|
let name = mem::take(&mut form.name);
|
||||||
let email = mem::take(&mut form.email);
|
let email = mem::take(&mut form.email);
|
||||||
let telefon = mem::take(&mut form.telefon);
|
let telefon = mem::take(&mut form.telefon);
|
||||||
let message = mem::take(&mut form.message);
|
let message = mem::take(&mut form.message);
|
||||||
|
|
||||||
let params = Query(IndexParams {
|
let params = IndexParams {
|
||||||
was_validated: Some(true),
|
config,
|
||||||
|
captcha_solutions,
|
||||||
|
was_validated: true,
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
email: Some(email),
|
email: Some(email),
|
||||||
telefon: Some(telefon),
|
telefon: Some(telefon),
|
||||||
message: Some(message),
|
message: Some(message),
|
||||||
});
|
error_message: Some(error_message),
|
||||||
|
};
|
||||||
|
|
||||||
index(params, config, captcha_solutions).await
|
render_contact_form(params).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn submit(
|
pub async fn submit(
|
||||||
|
State(config): State<Arc<config::Config>>,
|
||||||
|
State(captcha_solutions): State<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
||||||
|
State(mailer): State<Arc<mailer::Mailer>>,
|
||||||
Form(form): Form<forms::ContactForm>,
|
Form(form): Form<forms::ContactForm>,
|
||||||
config: Extension<Arc<config::Config>>,
|
|
||||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
|
||||||
mailer: Extension<Arc<mailer::Mailer>>,
|
|
||||||
) -> Result<Response, errors::AppError> {
|
) -> Result<Response, errors::AppError> {
|
||||||
if !captcha_solutions.check_answer(form.id, &form.captcha_answer) {
|
if !captcha_solutions.check_answer(form.id, &form.captcha_answer) {
|
||||||
return back_to_index(form, config, captcha_solutions).await;
|
info!("Wrong CAPTCHA");
|
||||||
|
return failed_submission(
|
||||||
|
config.clone(),
|
||||||
|
captcha_solutions,
|
||||||
|
&config.error_messages.captcha_error,
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
match mailer.send(&form.name, &form.email, &form.telefon, &form.message) {
|
match mailer.send(&form.name, &form.email, &form.telefon, &form.message) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return back_to_index(form, config, captcha_solutions).await;
|
error!("{e:?}");
|
||||||
|
return failed_submission(
|
||||||
|
config.clone(),
|
||||||
|
captcha_solutions,
|
||||||
|
&config.error_messages.email_error,
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
success(config).await
|
info!("Successful contact form submission");
|
||||||
|
success(State(config)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn success(config: Extension<Arc<config::Config>>) -> Result<Response, errors::AppError> {
|
pub async fn success(
|
||||||
|
State(config): State<Arc<config::Config>>,
|
||||||
|
) -> Result<Response, errors::AppError> {
|
||||||
let template = templates::Success {
|
let template = templates::Success {
|
||||||
path_prefix: &config.path_prefix,
|
path_prefix: &config.path_prefix,
|
||||||
message:
|
message: &config.strings.success,
|
||||||
"Ihre Anfrage wurde erfolgreich übermittelt! Wir melden uns bald bei Ihnen zurück.",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(template.into_response())
|
Ok(template.into_response())
|
||||||
|
|
11
src/states.rs
Normal file
11
src/states.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use axum::extract::FromRef;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{captcha_solutions, config, mailer};
|
||||||
|
|
||||||
|
#[derive(Clone, FromRef)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: Arc<config::Config>,
|
||||||
|
pub mailer: Arc<mailer::Mailer>,
|
||||||
|
pub captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>,
|
||||||
|
}
|
|
@ -1,16 +1,20 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "contact_form.askama.html")]
|
#[template(path = "contact_form.askama.html")]
|
||||||
pub struct ContactForm<'a> {
|
pub struct ContactForm<'a> {
|
||||||
pub path_prefix: &'a str,
|
pub path_prefix: &'a str,
|
||||||
pub was_validated: bool,
|
pub was_validated: bool,
|
||||||
pub id: u16,
|
pub id: u16,
|
||||||
pub name: &'a str,
|
pub name: String,
|
||||||
pub email: &'a str,
|
pub email: String,
|
||||||
pub telefon: &'a str,
|
pub telefon: String,
|
||||||
pub message: &'a str,
|
pub message: String,
|
||||||
pub captcha: &'a str,
|
pub captcha: String,
|
||||||
|
pub error_message: &'a str,
|
||||||
|
pub strings: &'a config::Strings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
|
@ -1,41 +1,43 @@
|
||||||
{% extends "base.askama.html" %}
|
{% extends "base.askama.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>
|
{% if error_message.len() > 0 %}
|
||||||
Alternativ können Sie im unteren Formular Ihre E-Mail-Adresse (und optional Telefon-Nummer) mit einer Nachricht mit Ihrem Anliegen hinterlassen. Wir versuchen dann, Sie so früh wie möglich zu kontaktieren.
|
<div class="alert alert-warning" role="alert">{{ error_message }}</div>
|
||||||
</p>
|
{% endif %}
|
||||||
|
|
||||||
<h2>Kontakt-Formular</h2>
|
<p>{{ strings.description }}</p>
|
||||||
|
|
||||||
|
<h2>{{ strings.title }}</h2>
|
||||||
|
|
||||||
<form id="contact-form"
|
<form id="contact-form"
|
||||||
action="{{ path_prefix }}/submit"
|
action="{{ path_prefix }}/"
|
||||||
method="post"
|
method="post"
|
||||||
class="{% if was_validated %}was-validated{% endif %}"
|
{% if was_validated %} class="was-validated"{% endif %}
|
||||||
novalidate>
|
novalidate>
|
||||||
<input type="hidden" name="id" value="{{ id }}" required>
|
<input type="hidden" name="id" value="{{ id }}" required>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">{{ strings.name_field.label }}</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="name"
|
name="name"
|
||||||
value="{{ name }}"
|
value="{{ name }}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="exampleInputEmail1"
|
id="exampleInputEmail1"
|
||||||
required>
|
required>
|
||||||
<div class="invalid-feedback">Geben Sie bitte Ihren Namen ein</div>
|
<div class="invalid-feedback">{{ strings.name_field.invalid_feedback }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">E-Mail</label>
|
<label for="email" class="form-label">{{ strings.email_field.label }}</label>
|
||||||
<input type="email"
|
<input type="email"
|
||||||
name="email"
|
name="email"
|
||||||
value="{{ email }}"
|
value="{{ email }}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="email"
|
id="email"
|
||||||
required>
|
required>
|
||||||
<div class="invalid-feedback">Geben Sie bitte Ihre E-Mail-Adresse ein</div>
|
<div class="invalid-feedback">{{ strings.email_field.invalid_feedback }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telefon" class="form-label">Telefon (optional)</label>
|
<label for="telefon" class="form-label">{{ strings.telefon_field_label }}</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="telefon"
|
name="telefon"
|
||||||
value="{{ telefon }}"
|
value="{{ telefon }}"
|
||||||
|
@ -43,26 +45,26 @@
|
||||||
id="telefon">
|
id="telefon">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="message" class="form-label">Nachricht</label>
|
<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>{{ message }}</textarea>
|
<textarea name="message" rows="5" class="form-control" id="message" style="resize: none;" required>{{ message }}</textarea>
|
||||||
<div class="invalid-feedback">Geben Sie bitte eine Nachricht mit Ihrem Anliegen ein</div>
|
<div class="invalid-feedback">{{ strings.message_field.invalid_feedback }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<img src="data:image/png;base64,{{ captcha }}">
|
<img src="data:image/png;base64,{{ captcha }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="captcha_answer" class="form-label">Code vom oberen Bild eingeben</label>
|
<label for="captcha_answer" class="form-label">{{ strings.captcha_field.label }}</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="captcha_answer"
|
name="captcha_answer"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="captcha_answer"
|
id="captcha_answer"
|
||||||
required>
|
required>
|
||||||
<div class="invalid-feedback">Geben Sie bitte den Code vom oberen Bild ein</div>
|
<div class="invalid-feedback">{{ strings.captcha_field.invalid_feedback }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-primary">Abschicken</button>
|
<button type="submit" class="btn btn-primary">{{ strings.submit }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -70,15 +72,15 @@
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const form = document.querySelector("#contact-form");
|
const form = document.querySelector("form#contact-form");
|
||||||
|
|
||||||
form.addEventListener("submit", (event) => {
|
form.addEventListener("submit", (event) => {
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
form.classList.add("was-validated");
|
form.classList.add("was-validated");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue