working email and confirmation
This commit is contained in:
parent
00aae2dfca
commit
87c0a2d541
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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}
|
||||||
|
@ -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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
28
terminwahl_back/src/handlebars_helper.rs
Normal file
28
terminwahl_back/src/handlebars_helper.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod handlebars_helper;
|
||||||
|
pub mod views;
|
||||||
|
@ -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"))
|
||||||
})
|
})
|
||||||
|
29
terminwahl_back/src/views.rs
Normal file
29
terminwahl_back/src/views.rs
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
29
terminwahl_back/templates/confirmed.html.hbs
Normal file
29
terminwahl_back/templates/confirmed.html.hbs
Normal 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>
|
@ -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
|
Loading…
Reference in New Issue
Block a user