Migrate to Axum

This commit is contained in:
Mo 2022-10-31 02:13:42 +01:00
parent c25d431cf3
commit 5b007a3220
8 changed files with 124 additions and 96 deletions

View file

@ -8,9 +8,10 @@ license-file = "LICENSE.txt"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
axum = "0.5"
captcha = "0.0.9" captcha = "0.0.9"
lettre = "0.10" 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 = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tera = "1.17"
tokio = { version = "1.21", features = ["full"] }

View file

@ -1,6 +0,0 @@
[default]
template_dir = "templates"
[release]
address = "0.0.0.0"
port = 80

View file

@ -1,7 +1,7 @@
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
pub struct ContactFormContext<'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,

View file

@ -1,6 +1,6 @@
use rocket::form::FromForm; use serde::Deserialize;
#[derive(FromForm)] #[derive(Deserialize)]
pub struct ContactForm { pub struct ContactForm {
pub id: u16, pub id: u16,
pub name: String, pub name: String,

View file

@ -5,30 +5,45 @@ mod forms;
mod mailer; mod mailer;
mod routes; mod routes;
use anyhow::Result; use anyhow::{Context, Result};
use rocket::{Build, Rocket}; use axum::extract::Extension;
use rocket_dyn_templates::Template; use axum::routing::{get, post};
use axum::{Router, Server};
use std::process; use std::process;
use std::sync::Arc;
use tera::Tera;
async fn init() -> Result<()> {
let tera = Arc::new(Tera::new("templates/*").context("Failed to parse templates!")?);
fn init() -> Result<Rocket<Build>> {
let mut config = config::Config::new()?; 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 rocket = rocket::build() let routes = Router::new()
.mount( .route("/", get(routes::index))
&config.path_prefix, .route("/submit", post(routes::submit))
rocket::routes![routes::index, routes::submit, routes::success], .route("/success", get(routes::success))
) .layer(Extension(config))
.manage(captcha_solutions::SharedCaptchaSolutions::new()) .layer(Extension(mailer))
.manage(mailer::Mailer::new(&mut config)?) .layer(Extension(captcha_solutions))
.manage(config) .layer(Extension(tera));
.attach(Template::fairing());
Ok(rocket) let app = Router::new().nest(&path_prefix, routes);
Server::bind(&([127, 0, 0, 1], 8080).into())
.serve(app.into_make_service())
.await
.unwrap();
Ok(())
} }
#[rocket::launch] #[tokio::main]
fn rocket() -> _ { async fn main() {
init().unwrap_or_else(|e| { init().await.unwrap_or_else(|e| {
eprintln!("{e}"); eprintln!("{e}");
process::exit(1); process::exit(1);
}) })

View file

@ -1,97 +1,115 @@
use rocket::form::{Form, Strict}; use anyhow::{Context, Result};
use rocket::response::status::BadRequest; use axum::extract::{Extension, Form, Query};
use rocket::response::Redirect; use axum::http::StatusCode;
use rocket::{get, post, uri, State}; use axum::response::{Html, IntoResponse, Response};
use rocket_dyn_templates::{context, Template}; use serde::Deserialize;
use std::mem; use std::mem;
use std::sync::Arc;
use tera::Tera;
use crate::captcha_solutions; use crate::{captcha_solutions, config, context, forms, mailer};
use crate::config;
use crate::context;
use crate::forms;
use crate::mailer;
#[get("/?<was_validated>&<name>&<email>&<telefon>&<message>")] pub struct AppError(anyhow::Error);
pub fn index(
impl IntoResponse for AppError {
fn into_response(self) -> Response {
StatusCode::BAD_REQUEST.into_response()
}
}
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>, was_validated: Option<bool>,
name: Option<&str>, name: Option<String>,
email: Option<&str>, email: Option<String>,
telefon: Option<&str>, telefon: Option<String>,
message: Option<&str>, message: Option<String>,
config: &State<config::Config>, }
captcha_solutions: &State<captcha_solutions::SharedCaptchaSolutions>,
) -> Result<Template, BadRequest<()>> { 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 = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy);
let captcha_base64 = match captcha.as_base64() { let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?;
Some(s) => s,
None => return Err(BadRequest(None)),
};
let id = captcha_solutions.store_solution(&captcha.chars_as_string()); let id = captcha_solutions.store_solution(&captcha.chars_as_string());
let form_context = context::ContactFormContext { let form_context = context::ContactForm {
path_prefix: &config.path_prefix, path_prefix: &config.path_prefix,
was_validated: was_validated.unwrap_or(false), was_validated: params.was_validated.unwrap_or(false),
id, id,
name: name.unwrap_or(""), name: &params.name.unwrap_or_default(),
email: email.unwrap_or(""), email: &params.email.unwrap_or_default(),
telefon: telefon.unwrap_or(""), telefon: &params.telefon.unwrap_or_default(),
message: message.unwrap_or(""), message: &params.message.unwrap_or_default(),
captcha: &captcha_base64, 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>")] async fn back_to_index(
pub fn submit( mut form: forms::ContactForm,
mut form: Form<Strict<forms::ContactForm>>, engine: Extension<Arc<Tera>>,
config: &State<config::Config>, config: Extension<Arc<config::Config>>,
captcha_solutions: &State<captcha_solutions::SharedCaptchaSolutions>, captcha_solutions: Extension<Arc<captcha_solutions::SharedCaptchaSolutions>>,
mailer: &State<mailer::Mailer>, ) -> Result<Response, AppError> {
) -> Redirect {
let path_prefix = config.path_prefix.clone();
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);
if !captcha_solutions.check_answer(form.id, &form.captcha_answer) { let params = Query(IndexParams {
return Redirect::to( was_validated: Some(true),
path_prefix name: Some(name),
+ &uri!(index( email: Some(email),
Some(true), telefon: Some(telefon),
Some(&name), message: Some(message),
Some(&email), });
Some(&telefon),
Some(&message) index(params, engine, config, captcha_solutions).await
))
.to_string(),
);
} }
match mailer.send(&name, &email, &telefon, &message) { 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 back_to_index(form, engine, config, captcha_solutions).await;
}
match mailer.send(&form.name, &form.email, &form.telefon, &form.message) {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => {
return Redirect::to( return back_to_index(form, engine, config, captcha_solutions).await;
path_prefix
+ &uri!(index(
Some(true),
Some(&name),
Some(&email),
Some(&telefon),
Some(&message)
))
.to_string(),
)
} }
} }
Redirect::to(path_prefix + &uri!(success()).to_string()) success(engine).await
} }
#[get("/success")] pub async fn success(engine: Extension<Arc<Tera>>) -> Result<Response, AppError> {
pub fn success() -> Template { let html = engine.render("success.html.tera", &tera::Context::new())?;
Template::render("success", context! {})
Ok(Html(html).into_response())
} }

View file

@ -1,4 +1,4 @@
{% extends "base" %} {% extends "base.html.tera" %}
{% block body %} {% block body %}
<p> <p>

View file

@ -1,4 +1,4 @@
{% extends "base" %} {% extends "base.html.tera" %}
{% block body %} {% block body %}
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">