Compare commits
10 commits
cd90c982f6
...
5b007a3220
Author | SHA1 | Date | |
---|---|---|---|
5b007a3220 | |||
c25d431cf3 | |||
635e7e4266 | |||
57471181f3 | |||
f3f5ce5145 | |||
a1b5d43ac5 | |||
dcfd6fe075 | |||
288f1d9f1b | |||
7d83419389 | |||
9dc33eda1a |
11 changed files with 241 additions and 147 deletions
|
@ -8,9 +8,10 @@ license-file = "LICENSE.txt"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.5"
|
||||
captcha = "0.0.9"
|
||||
lettre = "0.10"
|
||||
rocket = "0.5.0-rc.2"
|
||||
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tera = "1.17"
|
||||
tokio = { version = "1.21", features = ["full"] }
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
[default]
|
||||
template_dir = "templates"
|
||||
|
||||
[release]
|
||||
address = "0.0.0.0"
|
||||
port = 80
|
|
@ -1,3 +1,4 @@
|
|||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
|
@ -26,18 +27,17 @@ pub struct Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
pub fn new() -> Result<Self> {
|
||||
let config_file_var = "CF_CONFIG_FILE";
|
||||
let config_path = env::var(config_file_var)
|
||||
.unwrap_or_else(|_| panic!("Environment variable {config_file_var} missing!"));
|
||||
.with_context(|| format!("Environment variable {config_file_var} missing!"))?;
|
||||
|
||||
let config_file = File::open(&config_path).unwrap_or_else(|e| {
|
||||
panic!("Can not open the config file at the path {config_path}: {e}")
|
||||
});
|
||||
let config_file = File::open(&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: Self =
|
||||
serde_json::from_reader(config_reader).expect("Can not parse the config file as JSON!");
|
||||
let config: Self = serde_json::from_reader(config_reader)
|
||||
.context("Can not parse the config file as JSON!")?;
|
||||
|
||||
config
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
|
13
src/context.rs
Normal file
13
src/context.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContactForm<'a> {
|
||||
pub path_prefix: &'a str,
|
||||
pub was_validated: bool,
|
||||
pub id: u16,
|
||||
pub name: &'a str,
|
||||
pub email: &'a str,
|
||||
pub telefon: &'a str,
|
||||
pub message: &'a str,
|
||||
pub captcha: &'a str,
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use rocket::form::FromForm;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(FromForm)]
|
||||
#[derive(Deserialize)]
|
||||
pub struct ContactForm {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
|
|
|
@ -50,8 +50,8 @@ impl Mailer {
|
|||
|
||||
let subject = "Message from ".to_string() + &name;
|
||||
|
||||
let body = if telefon.len() > 0 {
|
||||
message + "\n\nTelefon: " + &telefon
|
||||
let body = if !telefon.is_empty() {
|
||||
message + "\n\nTelefon: " + telefon
|
||||
} else {
|
||||
message
|
||||
};
|
||||
|
|
54
src/main.rs
54
src/main.rs
|
@ -1,22 +1,50 @@
|
|||
mod captcha_solutions;
|
||||
mod config;
|
||||
mod context;
|
||||
mod forms;
|
||||
mod mailer;
|
||||
mod routes;
|
||||
|
||||
use rocket_dyn_templates::Template;
|
||||
use anyhow::{Context, Result};
|
||||
use axum::extract::Extension;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Router, Server};
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use tera::Tera;
|
||||
|
||||
#[rocket::launch]
|
||||
fn rocket() -> _ {
|
||||
let mut config = config::Config::new();
|
||||
async fn init() -> Result<()> {
|
||||
let tera = Arc::new(Tera::new("templates/*").context("Failed to parse templates!")?);
|
||||
|
||||
rocket::build()
|
||||
.mount(
|
||||
&config.path_prefix,
|
||||
rocket::routes![routes::index, routes::submit, routes::success],
|
||||
)
|
||||
.manage(captcha_solutions::SharedCaptchaSolutions::new())
|
||||
.manage(mailer::Mailer::new(&mut config).expect("Failed to create mailer!"))
|
||||
.manage(config)
|
||||
.attach(Template::fairing())
|
||||
let mut config = config::Config::new()?;
|
||||
let path_prefix = config.path_prefix.clone();
|
||||
let mailer = Arc::new(mailer::Mailer::new(&mut config)?);
|
||||
let config = Arc::new(config);
|
||||
let captcha_solutions = Arc::new(captcha_solutions::SharedCaptchaSolutions::new());
|
||||
|
||||
let routes = Router::new()
|
||||
.route("/", get(routes::index))
|
||||
.route("/submit", post(routes::submit))
|
||||
.route("/success", get(routes::success))
|
||||
.layer(Extension(config))
|
||||
.layer(Extension(mailer))
|
||||
.layer(Extension(captcha_solutions))
|
||||
.layer(Extension(tera));
|
||||
|
||||
let app = Router::new().nest(&path_prefix, routes);
|
||||
|
||||
Server::bind(&([127, 0, 0, 1], 8080).into())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
init().await.unwrap_or_else(|e| {
|
||||
eprintln!("{e}");
|
||||
process::exit(1);
|
||||
})
|
||||
}
|
||||
|
|
155
src/routes.rs
155
src/routes.rs
|
@ -1,104 +1,115 @@
|
|||
use rocket::form::{Form, Strict};
|
||||
use rocket::response::status::BadRequest;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::{get, post, uri, State};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
use anyhow::{Context, Result};
|
||||
use axum::extract::{Extension, Form, Query};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use serde::Deserialize;
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use tera::Tera;
|
||||
|
||||
use crate::captcha_solutions;
|
||||
use crate::config;
|
||||
use crate::forms;
|
||||
use crate::mailer;
|
||||
use crate::{captcha_solutions, config, context, forms, mailer};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FormContext<'a> {
|
||||
path_prefix: &'a str,
|
||||
id: u16,
|
||||
name: Option<&'a str>,
|
||||
email: Option<&'a str>,
|
||||
telefon: Option<&'a str>,
|
||||
message: Option<&'a str>,
|
||||
captcha: &'a str,
|
||||
pub struct AppError(anyhow::Error);
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/?<name>&<email>&<telefon>&<message>")]
|
||||
pub fn index(
|
||||
name: Option<&str>,
|
||||
email: Option<&str>,
|
||||
telefon: Option<&str>,
|
||||
message: Option<&str>,
|
||||
config: &State<config::Config>,
|
||||
captcha_solutions: &State<captcha_solutions::SharedCaptchaSolutions>,
|
||||
) -> Result<Template, BadRequest<()>> {
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IndexParams {
|
||||
was_validated: Option<bool>,
|
||||
name: Option<String>,
|
||||
email: Option<String>,
|
||||
telefon: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn index(
|
||||
Query(params): Query<IndexParams>,
|
||||
engine: Extension<Arc<Tera>>,
|
||||
config: Extension<Arc<config::Config>>,
|
||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
||||
) -> Result<Response, AppError> {
|
||||
let captcha = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy);
|
||||
let captcha_base64 = match captcha.as_base64() {
|
||||
Some(s) => s,
|
||||
None => return Err(BadRequest(None)),
|
||||
};
|
||||
let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?;
|
||||
|
||||
let id = captcha_solutions.store_solution(&captcha.chars_as_string());
|
||||
|
||||
let form_context = FormContext {
|
||||
let form_context = context::ContactForm {
|
||||
path_prefix: &config.path_prefix,
|
||||
was_validated: params.was_validated.unwrap_or(false),
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
telefon,
|
||||
message,
|
||||
name: ¶ms.name.unwrap_or_default(),
|
||||
email: ¶ms.email.unwrap_or_default(),
|
||||
telefon: ¶ms.telefon.unwrap_or_default(),
|
||||
message: ¶ms.message.unwrap_or_default(),
|
||||
captcha: &captcha_base64,
|
||||
};
|
||||
|
||||
Ok(Template::render("form", &form_context))
|
||||
let html = engine.render(
|
||||
"form.html.tera",
|
||||
&tera::Context::from_serialize(&form_context)?,
|
||||
)?;
|
||||
|
||||
Ok(Html(html).into_response())
|
||||
}
|
||||
|
||||
#[post("/submit", data = "<form>")]
|
||||
pub fn submit(
|
||||
mut form: Form<Strict<forms::ContactForm>>,
|
||||
config: &State<config::Config>,
|
||||
captcha_solutions: &State<captcha_solutions::SharedCaptchaSolutions>,
|
||||
mailer: &State<mailer::Mailer>,
|
||||
) -> Redirect {
|
||||
let path_prefix = config.path_prefix.clone();
|
||||
|
||||
async fn back_to_index(
|
||||
mut form: forms::ContactForm,
|
||||
engine: Extension<Arc<Tera>>,
|
||||
config: Extension<Arc<config::Config>>,
|
||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
||||
) -> Result<Response, AppError> {
|
||||
let name = mem::take(&mut form.name);
|
||||
let email = mem::take(&mut form.email);
|
||||
let telefon = mem::take(&mut form.telefon);
|
||||
let message = mem::take(&mut form.message);
|
||||
|
||||
let params = Query(IndexParams {
|
||||
was_validated: Some(true),
|
||||
name: Some(name),
|
||||
email: Some(email),
|
||||
telefon: Some(telefon),
|
||||
message: Some(message),
|
||||
});
|
||||
|
||||
index(params, engine, config, captcha_solutions).await
|
||||
}
|
||||
|
||||
pub async fn submit(
|
||||
Form(form): Form<forms::ContactForm>,
|
||||
engine: Extension<Arc<Tera>>,
|
||||
config: Extension<Arc<config::Config>>,
|
||||
captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
|
||||
mailer: Extension<Arc<mailer::Mailer>>,
|
||||
) -> Result<Response, AppError> {
|
||||
if !captcha_solutions.check_answer(form.id, &form.captcha_answer) {
|
||||
return Redirect::to(
|
||||
path_prefix
|
||||
+ &uri!(index(
|
||||
Some(&name),
|
||||
Some(&email),
|
||||
Some(&telefon),
|
||||
Some(&message)
|
||||
))
|
||||
.to_string(),
|
||||
);
|
||||
return back_to_index(form, engine, config, captcha_solutions).await;
|
||||
}
|
||||
|
||||
match mailer.send(&name, &email, &telefon, &message) {
|
||||
match mailer.send(&form.name, &form.email, &form.telefon, &form.message) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
return Redirect::to(
|
||||
path_prefix
|
||||
+ &uri!(index(
|
||||
Some(&name),
|
||||
Some(&email),
|
||||
Some(&telefon),
|
||||
Some(&message)
|
||||
))
|
||||
.to_string(),
|
||||
)
|
||||
return back_to_index(form, engine, config, captcha_solutions).await;
|
||||
}
|
||||
}
|
||||
|
||||
Redirect::to(path_prefix + &uri!(success()).to_string())
|
||||
success(engine).await
|
||||
}
|
||||
|
||||
#[get("/success")]
|
||||
pub fn success() -> &'static str {
|
||||
"Ihre Anfrage wurde übermittelt! Wir melden uns bald bei Ihnen zurück."
|
||||
pub async fn success(engine: Extension<Arc<Tera>>) -> Result<Response, AppError> {
|
||||
let html = engine.render("success.html.tera", &tera::Context::new())?;
|
||||
|
||||
Ok(Html(html).into_response())
|
||||
}
|
||||
|
|
20
templates/base.html.tera
Normal file
20
templates/base.html.tera
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Contact form</title>
|
||||
|
||||
{% block styles %}{% endblock %}
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,51 +1,71 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Contact form</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
{% extends "base.html.tera" %}
|
||||
|
||||
<h2>Kontakt-Formular</h2>
|
||||
{% block body %}
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<form action="{{ path_prefix }}/submit" method="post">
|
||||
<input type="hidden" name="id" value="{{ id }}" required>
|
||||
<h2>Kontakt-Formular</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" name="name" value="{{ name }}" class="form-control" id="exampleInputEmail1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">E-Mail</label>
|
||||
<input type="email" name="email" value="{{ email }}" class="form-control" id="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="telefon" class="form-label">Telefon</label>
|
||||
<input type="text" name="telefon" value="{{ telefon }}" class="form-control" id="telefon">
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="message" class="form-label">Nachricht</label>
|
||||
<textarea name="message" rows="5" class="form-control" id="message" style="resize: none;" required>{{ message }}</textarea>
|
||||
</div>
|
||||
<form id="contact-form" action="{{ path_prefix }}/submit" method="post" class="{% if was_validated %}was-validated{% endif %}" novalidate>
|
||||
<input type="hidden" name="id" value="{{ id }}" required>
|
||||
|
||||
<div class="mb-3">
|
||||
<img src="data:image/png;base64,{{ captcha }}">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" name="name" value="{{ name }}" class="form-control" id="exampleInputEmail1" required>
|
||||
<div class="invalid-feedback">
|
||||
Geben Sie bitte Ihren Namen ein
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="captcha_answer" class="form-label">Code vom oberen Bild eingeben</label>
|
||||
<input type="text" name="captcha_answer" class="form-control" id="captcha_answer" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">E-Mail</label>
|
||||
<input type="email" name="email" value="{{ email }}" class="form-control" id="email" required>
|
||||
<div class="invalid-feedback">
|
||||
Geben Sie bitte Ihre E-Mail-Adresse ein
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="telefon" class="form-label">Telefon (optional)</label>
|
||||
<input type="text" name="telefon" value="{{ telefon }}" class="form-control" id="telefon">
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="message" class="form-label">Nachricht</label>
|
||||
<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>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Abschicken</button>
|
||||
<div class="mb-3">
|
||||
<img src="data:image/png;base64,{{ captcha }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="captcha_answer" class="form-label">Code vom oberen Bild eingeben</label>
|
||||
<input type="text" name="captcha_answer" class="form-control" id="captcha_answer" required>
|
||||
<div class="invalid-feedback">
|
||||
Geben Sie bitte den Code vom oberen Bild ein
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Abschicken</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const form = document.querySelector("#contact-form");
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
form.classList.add("was-validated");
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
7
templates/success.html.tera
Normal file
7
templates/success.html.tera
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html.tera" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
Ihre Anfrage wurde erfolgreich übermittelt! Wir melden uns bald bei Ihnen zurück.
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue