emails funktionieren

This commit is contained in:
Franz Dietrich 2023-01-31 22:26:26 +01:00
parent c708a4b79b
commit 00aae2dfca
Signed by: dietrich
GPG Key ID: F0CE5A20AB5C4B27
12 changed files with 325 additions and 88 deletions

3
.env
View File

@ -1,3 +1,4 @@
DATABASE_URL="sqlite://db.sqlite"
DATABASE_URL="sqlite://terminwahl_back/db.sqlite"
RUST_LOG="debug"
SMTP_USER="SMTP_USERNAME"
SMTP_PASSWORD="SMTP_PASSWORD"

159
Cargo.lock generated
View File

@ -329,9 +329,9 @@ checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "async-trait"
version = "0.1.63"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1"
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
dependencies = [
"proc-macro2",
"quote",
@ -430,9 +430,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "bytestring"
@ -445,9 +445,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
@ -545,9 +545,9 @@ dependencies = [
[[package]]
name = "crc"
version = "3.0.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3"
checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
dependencies = [
"crc-catalog",
]
@ -805,9 +805,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [
"futures-channel",
"futures-core",
@ -819,9 +819,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
"futures-sink",
@ -829,15 +829,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
[[package]]
name = "futures-executor"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
dependencies = [
"futures-core",
"futures-task",
@ -857,15 +857,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
[[package]]
name = "futures-macro"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
dependencies = [
"proc-macro2",
"quote",
@ -874,21 +874,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
[[package]]
name = "futures-task"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
[[package]]
name = "futures-util"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
"futures-channel",
"futures-core",
@ -1117,6 +1117,21 @@ dependencies = [
"tracing",
]
[[package]]
name = "handlebars"
version = "4.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a"
dependencies = [
"log",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror",
"walkdir",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1618,6 +1633,50 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ab62d2fa33726dbe6321cc97ef96d8cde531e3eeaf858a058de53a8a6d40d8f"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf026e2d0581559db66d837fe5242320f525d85c76283c61f4d51a1238d65ea"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b27bd18aa01d91c8ed2b61ea23406a676b42d82609c6e2581fba42f0c15f17f"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02b677c1859756359fc9983c2e56a0237f18624a3789528804406b7e915e5d"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "pin-project"
version = "1.0.12"
@ -1887,6 +1946,15 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -2185,8 +2253,10 @@ dependencies = [
"chrono",
"dotenv",
"env_logger",
"handlebars",
"lettre",
"log",
"rand",
"serde",
"serde_json",
"sqlx",
@ -2292,9 +2362,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.24.2"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [
"autocfg",
"bytes",
@ -2384,6 +2454,12 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.6.0"
@ -2416,9 +2492,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
@ -2481,6 +2557,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -2718,18 +2805,18 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.12.2+zstd.1.5.2"
version = "0.12.3+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9262a83dc741c0b0ffec209881b45dbc232c21b02a2b9cb1adb93266e41303d"
checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "6.0.2+zstd.1.5.2"
version = "6.0.3+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cf39f730b440bab43da8fb5faf5f254574462f73f260f85f7987f32154ff17"
checksum = "68e4a3f57d13d0ab7e478665c60f35e2a613dcd527851c2c7287ce5c787e134a"
dependencies = [
"libc",
"zstd-sys",
@ -2737,9 +2824,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.5+zstd.1.5.2"
version = "2.0.6+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc50ffce891ad571e9f9afe5039c4837bede781ac4bb13052ed7ae695518596"
checksum = "68a3f9792c0c3dc6c165840a75f47ae1f4da402c2d006881129579f6597e801b"
dependencies = [
"cc",
"libc",

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}

View File

@ -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)
);
@ -22,3 +25,9 @@ CREATE TABLE appointment_slots (
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
);

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(RequestState::Success))
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
}
}
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

@ -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

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(())
}

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))?

View File

@ -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

View File

@ -3,10 +3,9 @@ use std::collections::{HashMap, HashSet};
use gloo::console::log;
use requests::{fetch_slots, fetch_teachers, fetch_unavailable, send_appointments};
use serde::{Deserialize, Serialize};
use terminwahl_typen::{
AppointmentSlot, AppointmentSlots, IdType, PlannedAppointment, RequestState, SlotId, Teacher,
Teachers,
AppointmentSlot, AppointmentSlots, IdType, Nutzer, PlannedAppointment, RequestState, SlotId,
Teacher, Teachers,
};
use web_sys::HtmlInputElement;
use yew::prelude::*;
@ -38,13 +37,6 @@ pub struct App {
successfully_saved: Option<RequestState>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Nutzer {
name: String,
schüler: String,
email: String,
}
impl From<Result<Msg, Msg>> for Msg {
fn from(value: Result<Msg, Msg>) -> Self {
match value {
@ -54,12 +46,6 @@ impl From<Result<Msg, Msg>> for Msg {
}
}
impl Nutzer {
fn validate(&self) -> bool {
!self.name.is_empty() && !self.email.is_empty() && !self.schüler.is_empty()
}
}
impl Component for App {
type Message = Msg;
type Properties = ();
@ -137,7 +123,13 @@ impl Component for App {
if (1..=3).contains(&self.appointments.len()) {
let values = self.appointments.clone().into_values();
let appointments: Vec<PlannedAppointment> = values.collect();
ctx.link().send_future(send_appointments(appointments));
ctx.link().send_future(send_appointments(
appointments,
self.nutzer
.as_ref()
.expect("This should always exist")
.clone(),
));
true
} else {
true

View File

@ -1,5 +1,5 @@
use gloo::net::http::{Method, Request};
use terminwahl_typen::{PlannedAppointment, RequestState};
use terminwahl_typen::{Nutzer, PlannedAppointment, RequestState};
use crate::Msg;
@ -39,10 +39,13 @@ pub async fn fetch_unavailable() -> Result<Msg, Msg> {
Ok(Msg::ReceivedUnavailable(response))
}
pub async fn send_appointments(appointments: Vec<PlannedAppointment>) -> Result<Msg, Msg> {
pub async fn send_appointments(
appointments: Vec<PlannedAppointment>,
nutzer: Nutzer,
) -> Result<Msg, Msg> {
let response = Request::new("/send/appointments")
.method(Method::POST)
.json(&appointments)
.json(&(&appointments, &nutzer))
.map_err(|_| Msg::AppointmentsSent(RequestState::Error))?
.send()
.await;

View File

@ -71,3 +71,16 @@ pub enum RequestState {
Message(String),
Error,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Nutzer {
pub name: String,
pub schüler: String,
pub email: String,
}
impl Nutzer {
pub fn validate(&self) -> bool {
!self.name.is_empty() && !self.email.is_empty() && !self.schüler.is_empty()
}
}