emails funktionieren

This commit is contained in:
2023-01-31 22:26:26 +01:00
parent c708a4b79b
commit 00aae2dfca
12 changed files with 323 additions and 86 deletions
+2
View File
@@ -20,6 +20,8 @@ dotenv = "*"
env_logger = "0.10"
log = "*"
lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]}
rand = "*"
handlebars = {version="4.3", features=["dir_source"]}
terminwahl_typen={path="../terminwahl_typen/"}
serde = {workspace = true}
@@ -14,6 +14,9 @@ CREATE TABLE appointments (
id INTEGER PRIMARY KEY,
teacher_id INTEGER NOT NULL,
slot_id INTEGER NOT NULL,
nutzer_id INTEGER NOT NULL,
validation_key TEXT NOT NULL,
expires DATETIME,
FOREIGN KEY(teacher_id) REFERENCES teachers(id),
FOREIGN KEY(slot_id) REFERENCES appointment_slots(id)
);
@@ -21,4 +24,10 @@ CREATE TABLE appointment_slots (
id INTEGER PRIMARY KEY,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL
);
CREATE TABLE nutzer (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
schueler TEXT NOT NULL,
email TEXT NOT NULL
);
+87 -23
View File
@@ -1,37 +1,101 @@
use actix_web::{error, web, HttpResponse};
use lettre::{
transport::smtp::{authentication::Credentials, SmtpTransportBuilder},
Message, SmtpTransport, Transport,
};
use terminwahl_typen::{PlannedAppointment, RequestState};
use handlebars::Handlebars;
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
use log::debug;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde_json::json;
use terminwahl_typen::{Nutzer, PlannedAppointment, RequestState};
use crate::db::{self, Pool};
use crate::db::{self, write::confirm_appointments, Pool};
pub async fn save_appointments_json(
pool: web::Data<Pool>,
mailer: web::Data<SmtpTransport>,
appointments: web::Json<Vec<PlannedAppointment>>,
handlebars: web::Data<Handlebars<'_>>,
input: web::Json<(Vec<PlannedAppointment>, Nutzer)>,
) -> Result<HttpResponse, error::Error> {
db::write::save_appointments(&pool, &appointments)
debug!("Extracting data");
let (appointments, nutzer) = input.into_inner();
debug!("Saving user");
let nutzer_id = db::write::save_nutzer(&pool, &nutzer)
.await
.map_err(error::ErrorInternalServerError)?;
debug!("Saving appointments");
let validation_key: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
db::write::save_appointments(&pool, &appointments, nutzer_id, &validation_key)
.await
.map_err(error::ErrorInternalServerError)?;
let email = Message::builder()
.from(
"Franz Dietrich <franz.dietrich@uhlandshoehe.de>"
.parse()
.unwrap(),
)
.to("Franz Dietrich <dietrich@teilgedanken.de>".parse().unwrap())
.subject("Happy new year")
.body(String::from("Be happy!"))
.unwrap();
let mail_result = send_confirmation_request(&nutzer, &validation_key, &handlebars, &mailer);
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Ok(HttpResponse::Ok().json(mail_result))
}
pub fn send_confirmation_request(
nutzer: &Nutzer,
validation_key: &str,
handlebars: &Handlebars,
mailer: &SmtpTransport,
) -> RequestState {
let data = json! {
{
"nutzer": nutzer,
"validation_key": validation_key
}};
debug!("{:?}", handlebars.get_templates());
if let Ok(email_text) = handlebars.render("email_confirm", &data) {
let email = match Message::builder()
.from(
"Franz Dietrich <franz.dietrich@uhlandshoehe.de>"
.parse()
.expect("Should not fail"),
)
.to(
match format!("{} <{}>", nutzer.name, nutzer.email).parse() {
Ok(v) => v,
Err(_) => return RequestState::Error,
},
)
.subject("Elternsprechtag: Bestätigen Sie Ihre Termine")
.header(ContentType::TEXT_PLAIN)
.body(
match lettre::message::Body::new_with_encoding(
email_text,
lettre::message::header::ContentTransferEncoding::Base64,
) {
Ok(body) => body,
Err(_) => return RequestState::Error,
},
) {
Ok(message) => message,
Err(_) => return RequestState::Error,
};
// Send the email
match mailer.send(&email) {
Ok(_) => RequestState::Success,
Err(e) => {
debug!("Failed to send: {e}");
RequestState::Error
}
}
} else {
RequestState::Error
}
}
Ok(HttpResponse::Ok().json(RequestState::Success))
pub async fn confirm_validation_key(
pool: web::Data<Pool>,
mailer: web::Data<SmtpTransport>,
handlebars: web::Data<Handlebars<'_>>,
validation_key: web::Path<String>,
) -> Result<HttpResponse, error::Error> {
match confirm_appointments(&pool, &validation_key).await {
Ok(_) => Ok(HttpResponse::Ok().json(RequestState::Success)),
Err(e) => Err(error::ErrorBadRequest(e)),
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ pub async fn get_unavailable(db: &Pool) -> Result<HashSet<SlotId>, sqlx::Error>
SlotId,
r#"
SELECT teacher_id, slot_id
FROM `appointments`"#,
FROM `appointments` WHERE datetime(expires) > datetime('now');"#,
)
.fetch_all(db)
.await
+50 -5
View File
@@ -1,17 +1,62 @@
use terminwahl_typen::PlannedAppointment;
use chrono::{Duration, Local};
use sqlx::query;
use terminwahl_typen::{IdType, Nutzer, PlannedAppointment};
use super::Pool;
pub async fn save_appointments(
pool: &Pool,
appointments: &[PlannedAppointment],
nutzer_id: IdType,
validation_key: &str,
) -> Result<(), sqlx::Error> {
for appointment in appointments {
sqlx::query("INSERT INTO appointments (teacher_id, slot_id) VALUES ($1, $2)")
.bind(appointment.teacher_id)
.bind(appointment.slot_id)
let _ = query!("DELETE FROM appointments WHERE datetime(expires) < datetime('now');")
.execute(pool)
.await?;
.await;
let now = Local::now().naive_local();
let in_three_hours = now + Duration::hours(3);
query!(
"INSERT INTO appointments (teacher_id, slot_id, nutzer_id, validation_key, expires) VALUES ($1, $2, $3, $4, $5)",
appointment.teacher_id,
appointment.slot_id,
nutzer_id,
validation_key,
in_three_hours
)
.execute(pool)
.await?;
}
Ok(())
}
pub async fn save_nutzer(pool: &Pool, nutzer: &Nutzer) -> Result<IdType, sqlx::Error> {
query!(
"INSERT INTO nutzer (name, schueler, email) VALUES ($1, $2, $3)",
nutzer.name,
nutzer.schüler,
nutzer.email
)
.execute(pool)
.await?;
let db_nutzer = query!(
"SELECT id FROM nutzer WHERE name = ? and email = ?",
nutzer.name,
nutzer.email
)
.fetch_one(pool)
.await?;
Ok(db_nutzer.id)
}
pub async fn confirm_appointments(pool: &Pool, validation_key: &str) -> Result<(), sqlx::Error> {
let _ = query!(
"UPDATE appointments SET expires = NULL WHERE validation_key = ?",
validation_key
)
.execute(pool)
.await?;
Ok(())
}
+13
View File
@@ -7,7 +7,9 @@ use actix_web::{
web, App, HttpServer,
};
use dotenv::dotenv;
use handlebars::Handlebars;
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
use log::debug;
use std::env;
use terminwahl_back::{api, db};
@@ -30,7 +32,13 @@ async fn main() -> std::io::Result<()> {
// Connection pool settings
.build();
let mut handlebars = Handlebars::new();
handlebars
.register_templates_directory(".hbs", "terminwahl_back/templates")
.unwrap();
log::info!("starting HTTP server at http://localhost:8080");
debug!("{:?}", handlebars.get_templates());
HttpServer::new(move || {
log::debug!("Constructing the App");
@@ -48,6 +56,7 @@ async fn main() -> std::io::Result<()> {
App::new()
.app_data(web::Data::new(pool.clone()))
.app_data(web::Data::new(smtp_pool.clone()))
.app_data(web::Data::new(handlebars.clone()))
.wrap(Logger::default())
.wrap(session_store)
.wrap(error_handlers)
@@ -66,6 +75,10 @@ async fn main() -> std::io::Result<()> {
web::resource("/send/appointments")
.route(web::post().to(api::write::save_appointments_json)),
)
.service(
web::resource("/confirm/{validation_key}")
.route(web::get().to(api::write::confirm_validation_key)),
)
.service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html"))
})
.bind(("127.0.0.1", 8080))?
@@ -0,0 +1,8 @@
Sehr geehrte/r {{ nutzer.name }},
bitte bestätigen Sie durch klick auf den folgenden Link ihre Anmeldung für die Termine
https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }}
Vielen Dank für Ihre Anmeldung!
Das Oberstufenkollegium