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 = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -933,6 +934,12 @@ dependencies = [
"polyval",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gloo"
version = "0.8.0"
@ -2253,6 +2260,8 @@ dependencies = [
"chrono",
"dotenv",
"env_logger",
"futures",
"glob",
"handlebars",
"lettre",
"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
[dependencies]
futures = "*"
actix-web = "4.3"
actix-rt = "2.8"
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"]}
rand = "*"
handlebars = {version="4.3", features=["dir_source"]}
glob = "*"
terminwahl_typen={path="../terminwahl_typen/"}
serde = {workspace = true}

View File

@ -1,12 +1,40 @@
use std::error::Error;
use actix_web::{error, web, HttpResponse};
use futures::future;
use handlebars::Handlebars;
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
use log::debug;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Serialize;
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(
pool: web::Data<Pool>,
@ -30,12 +58,28 @@ pub async fn save_appointments_json(
.await
.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))
}
pub fn send_confirmation_request(
appointments: &Vec<FullAppointment>,
nutzer: &Nutzer,
validation_key: &str,
handlebars: &Handlebars,
@ -43,6 +87,7 @@ pub fn send_confirmation_request(
) -> RequestState {
let data = json! {
{
"appointments": appointments,
"nutzer": nutzer,
"validation_key": validation_key
}};
@ -87,15 +132,3 @@ pub fn send_confirmation_request(
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 terminwahl_typen::{
AppointmentSlot, AppointmentSlots, SlotId, Subject, Subjects, Teacher, Teachers,
AppointmentSlot, AppointmentSlots, IdType, SlotId, Subject, Subjects, Teacher, Teachers,
};
use super::Pool;
@ -19,6 +19,18 @@ pub async fn get_teachers(db: &Pool) -> Result<Teachers, sqlx::Error> {
.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> {
query_as!(
Subject,
@ -44,6 +56,21 @@ pub async fn get_slots(db: &Pool) -> Result<AppointmentSlots, sqlx::Error> {
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> {
match query_as!(
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 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 log::debug;
use std::env;
use terminwahl_back::{api, db};
use terminwahl_back::{api, db, handlebars_helper::TimeOfDate, views};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@ -33,6 +33,7 @@ async fn main() -> std::io::Result<()> {
.build();
let mut handlebars = Handlebars::new();
handlebars.register_helper("time_of", Box::new(TimeOfDate));
handlebars
.register_templates_directory(".hbs", "terminwahl_back/templates")
.unwrap();
@ -77,7 +78,7 @@ async fn main() -> std::io::Result<()> {
)
.service(
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"))
})

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
Alle Treffen bestätigen:
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!
Das Oberstufenkollegium