working email and confirmation

This commit is contained in:
Franz Dietrich 2023-02-01 22:28:55 +01:00
parent 00aae2dfca
commit 87c0a2d541
Signed by: dietrich
GPG Key ID: F0CE5A20AB5C4B27
10 changed files with 191 additions and 18 deletions

9
Cargo.lock generated
View File

@ -811,6 +811,7 @@ checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -933,6 +934,12 @@ dependencies = [
"polyval", "polyval",
] ]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "gloo" name = "gloo"
version = "0.8.0" version = "0.8.0"
@ -2253,6 +2260,8 @@ dependencies = [
"chrono", "chrono",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures",
"glob",
"handlebars", "handlebars",
"lettre", "lettre",
"log", "log",

View File

@ -6,6 +6,7 @@ default-run = "terminwahl_back"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
futures = "*"
actix-web = "4.3" actix-web = "4.3"
actix-rt = "2.8" actix-rt = "2.8"
actix-files = "0.6.2" actix-files = "0.6.2"
@ -22,6 +23,7 @@ log = "*"
lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]} lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]}
rand = "*" rand = "*"
handlebars = {version="4.3", features=["dir_source"]} handlebars = {version="4.3", features=["dir_source"]}
glob = "*"
terminwahl_typen={path="../terminwahl_typen/"} terminwahl_typen={path="../terminwahl_typen/"}
serde = {workspace = true} serde = {workspace = true}

View File

@ -1,12 +1,40 @@
use std::error::Error;
use actix_web::{error, web, HttpResponse}; use actix_web::{error, web, HttpResponse};
use futures::future;
use handlebars::Handlebars; use handlebars::Handlebars;
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport}; use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
use log::debug; use log::debug;
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Serialize;
use serde_json::json; use serde_json::json;
use terminwahl_typen::{Nutzer, PlannedAppointment, RequestState}; use terminwahl_typen::{
AppointmentSlot, IdType, Nutzer, PlannedAppointment, RequestState, Teacher,
};
use crate::db::{self, write::confirm_appointments, Pool}; use crate::db::{
self,
read::{get_slot_by_id, get_teacher_by_id},
write::confirm_appointments,
Pool,
};
#[derive(Serialize)]
pub struct FullAppointment {
pub teacher: Teacher,
pub slot: AppointmentSlot,
}
impl FullAppointment {
async fn get(pool: &Pool, teacher_id: i64, slot_id: i64) -> Result<Self, Box<dyn Error>> {
Ok(Self {
teacher: get_teacher_by_id(pool, teacher_id)
.await
.expect("Teacher not found"),
slot: get_slot_by_id(pool, slot_id).await.expect("Slot not found"),
})
}
}
pub async fn save_appointments_json( pub async fn save_appointments_json(
pool: web::Data<Pool>, pool: web::Data<Pool>,
@ -30,12 +58,28 @@ pub async fn save_appointments_json(
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let mail_result = send_confirmation_request(&nutzer, &validation_key, &handlebars, &mailer); let full_appointments = future::try_join_all(appointments.into_iter().map(
|PlannedAppointment {
teacher_id,
slot_id,
}| { FullAppointment::get(&pool, teacher_id, slot_id) },
))
.await
.expect("Failed to get full list of appointments");
let mail_result = send_confirmation_request(
&full_appointments,
&nutzer,
&validation_key,
&handlebars,
&mailer,
);
Ok(HttpResponse::Ok().json(mail_result)) Ok(HttpResponse::Ok().json(mail_result))
} }
pub fn send_confirmation_request( pub fn send_confirmation_request(
appointments: &Vec<FullAppointment>,
nutzer: &Nutzer, nutzer: &Nutzer,
validation_key: &str, validation_key: &str,
handlebars: &Handlebars, handlebars: &Handlebars,
@ -43,6 +87,7 @@ pub fn send_confirmation_request(
) -> RequestState { ) -> RequestState {
let data = json! { let data = json! {
{ {
"appointments": appointments,
"nutzer": nutzer, "nutzer": nutzer,
"validation_key": validation_key "validation_key": validation_key
}}; }};
@ -87,15 +132,3 @@ pub fn send_confirmation_request(
RequestState::Error RequestState::Error
} }
} }
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)),
}
}

View File

@ -3,7 +3,7 @@ use std::collections::HashSet;
use sqlx::query_as; use sqlx::query_as;
use terminwahl_typen::{ use terminwahl_typen::{
AppointmentSlot, AppointmentSlots, SlotId, Subject, Subjects, Teacher, Teachers, AppointmentSlot, AppointmentSlots, IdType, SlotId, Subject, Subjects, Teacher, Teachers,
}; };
use super::Pool; use super::Pool;
@ -19,6 +19,18 @@ pub async fn get_teachers(db: &Pool) -> Result<Teachers, sqlx::Error> {
.await .await
} }
pub async fn get_teacher_by_id(db: &Pool, teacher_id: IdType) -> Result<Teacher, sqlx::Error> {
query_as!(
Teacher,
r#"
SELECT *
FROM `teachers` WHERE id = ?"#,
teacher_id
)
.fetch_one(db)
.await
}
pub async fn get_subjects(db: &Pool) -> Result<Subjects, sqlx::Error> { pub async fn get_subjects(db: &Pool) -> Result<Subjects, sqlx::Error> {
query_as!( query_as!(
Subject, Subject,
@ -44,6 +56,21 @@ pub async fn get_slots(db: &Pool) -> Result<AppointmentSlots, sqlx::Error> {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
pub async fn get_slot_by_id(db: &Pool, slot_id: IdType) -> Result<AppointmentSlot, sqlx::Error> {
match query_as!(
AppointmentSlot,
r#"
SELECT *
FROM `appointment_slots` WHERE id = ?"#,
slot_id
)
.fetch_one(db)
.await
{
Ok(slot) => Ok(slot),
Err(e) => Err(e),
}
}
pub async fn get_unavailable(db: &Pool) -> Result<HashSet<SlotId>, sqlx::Error> { pub async fn get_unavailable(db: &Pool) -> Result<HashSet<SlotId>, sqlx::Error> {
match query_as!( match query_as!(
SlotId, SlotId,

View File

@ -0,0 +1,28 @@
use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext};
// implement by a structure impls HelperDef
#[derive(Clone, Copy)]
pub struct TimeOfDate;
impl HelperDef for TimeOfDate {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let date = h
.param(0)
.and_then(|v| v.value().as_str())
.unwrap_or("")
.to_owned();
let time_reversed = date.chars().rev();
let only_time: String = time_reversed.take(8).collect();
let only_minutes: String = only_time.chars().rev().take(5).collect();
out.write(&only_minutes)?;
Ok(())
}
}

View File

@ -1,2 +1,4 @@
pub mod api; pub mod api;
pub mod db; pub mod db;
pub mod handlebars_helper;
pub mod views;

View File

@ -11,7 +11,7 @@ use handlebars::Handlebars;
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport}; use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
use log::debug; use log::debug;
use std::env; use std::env;
use terminwahl_back::{api, db}; use terminwahl_back::{api, db, handlebars_helper::TimeOfDate, views};
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -33,6 +33,7 @@ async fn main() -> std::io::Result<()> {
.build(); .build();
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
handlebars.register_helper("time_of", Box::new(TimeOfDate));
handlebars handlebars
.register_templates_directory(".hbs", "terminwahl_back/templates") .register_templates_directory(".hbs", "terminwahl_back/templates")
.unwrap(); .unwrap();
@ -77,7 +78,7 @@ async fn main() -> std::io::Result<()> {
) )
.service( .service(
web::resource("/confirm/{validation_key}") web::resource("/confirm/{validation_key}")
.route(web::get().to(api::write::confirm_validation_key)), .route(web::get().to(views::confirm_validation_key)),
) )
.service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html")) .service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html"))
}) })

View File

@ -0,0 +1,29 @@
use actix_web::{error, web, HttpResponse};
use glob::glob;
use handlebars::Handlebars;
use lettre::SmtpTransport;
use serde_json::json;
use terminwahl_typen::RequestState;
use crate::db::{write::confirm_appointments, Pool};
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> {
let css_path = glob("terminwahl_front/dist/my_bulma_colors*.css")
.expect("Failed to find css file")
.next()
.expect("Failed to find file")
.expect("Failed to find file");
let css_file = css_path.file_name().unwrap().to_str();
let data = json!({
"css_file" : css_file,
});
match confirm_appointments(&pool, &validation_key).await {
Ok(_) => Ok(HttpResponse::Ok().body(handlebars.render("confirmed.html", &data).unwrap())),
Err(e) => Err(error::ErrorBadRequest(e)),
}
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Yew App</title>
<link rel="stylesheet" href="/{{ css_file }}" />
<meta name="keywords" content="Termine,Waldorfschule,Lehrersprechtag,Lehrerinnensprechtag" />
<meta name="author" content="Franz Dietrich" />
<meta name="description" content="Termine buchen für den Lehrersprechtag der Waldorfschule Uhlandshöhe" />
</head>
<body>
<section class="hero is-warning">
<div class="hero-body">
<p class="title has-text-link">
Elternsprechtag
</p>
<p class="subtitle">
Am 28.02.23
</p>
</div>
</section>
<div class="container">
<div class="section">
<h1 class="title is-1">Bestätigt</h1>
</div>
</div>
</body>
</html>

View File

@ -2,7 +2,20 @@ Sehr geehrte/r {{ nutzer.name }},
bitte bestätigen Sie durch klick auf den folgenden Link ihre Anmeldung für die Termine bitte bestätigen Sie durch klick auf den folgenden Link ihre Anmeldung für die Termine
Alle Treffen bestätigen:
https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }} https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }}
Wenn Sie ein Treffen absagen wollen klicken Sie trotzdem auf den obigen Bestätigungslink und zusätzlich auf den passenden Absage-Link unten.
Auch bestätigte Treffen können abgesagt werden.
Liste der Treffen - mit Absage-Links, sollten Sie zu einem Treffen doch nicht können.
{{#each appointments as |appoint|}}
{{appoint.teacher.ansprache}} {{appoint.teacher.last_name}}: {{time_of appoint.slot.start_time}} - {{time_of appoint.slot.end_time}}
Dieses Treffen Absagen:
https://elternsprechtag.uhle.cloud/cancel/{{appoint.teacher.id}}/{{appoint.slot.id}}/{{ ../validation_key }}
{{/each}}
Vielen Dank für Ihre Anmeldung! Vielen Dank für Ihre Anmeldung!
Das Oberstufenkollegium Das Oberstufenkollegium