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 = [
 | 
			
		||||
 "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",
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -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)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 db;
 | 
			
		||||
pub mod handlebars_helper;
 | 
			
		||||
pub mod views;
 | 
			
		||||
 | 
			
		||||
@ -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"))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user