diff --git a/migration.sql b/migration.sql index 7da1feb..ad652b5 100644 --- a/migration.sql +++ b/migration.sql @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" ( "phone" text, "address" text, "family_id" INTEGER REFERENCES family(id), - "membership_pdf" BLOB + "membership_pdf" BLOB, + "user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))) ); CREATE TABLE IF NOT EXISTS "family" ( diff --git a/src/model/event.rs b/src/model/event.rs index b62811b..cf5917d 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -183,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id", .unwrap() //TODO: fixme } + pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec { + let mut ret = Vec::new(); + let events = Self::all(db).await; + for event in events { + if event.is_rower_registered(db, user).await { + ret.push(event); + } + } + ret + } + //TODO: add tests pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool { let is_rower = sqlx::query!( @@ -394,38 +405,41 @@ WHERE trip_details.id=? let events = Event::all(db).await; for event in events { - let mut vevent = - ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000"); - vevent.push(DtStart::new(format!( - "{}T{}00", - event.day.replace('-', ""), - event.planned_starting_time.replace(':', "") - ))); - let tripdetails = event.trip_details(db).await; - let mut name = String::new(); - if event.is_cancelled() { - name.push_str("ABGESAGT"); - if let Some(notes) = &tripdetails.notes { - if !notes.is_empty() { - name.push_str(&format!(" (Grund: {notes})")) - } - } - - name.push_str("! :-( "); - } - name.push_str(&format!("{} ", event.name)); - - if let Some(triptype) = tripdetails.triptype(db).await { - name.push_str(&format!("• {} ", triptype.name)) - } - vevent.push(Summary::new(name)); - calendar.add_event(vevent); + calendar.add_event(event.get_vevent(db).await); } let mut buf = Vec::new(); write!(&mut buf, "{}", calendar).unwrap(); String::from_utf8(buf).unwrap() } + pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { + let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000"); + vevent.push(DtStart::new(format!( + "{}T{}00", + self.day.replace('-', ""), + self.planned_starting_time.replace(':', "") + ))); + let tripdetails = self.trip_details(db).await; + let mut name = String::new(); + if self.is_cancelled() { + name.push_str("ABGESAGT"); + if let Some(notes) = &tripdetails.notes { + if !notes.is_empty() { + name.push_str(&format!(" (Grund: {notes})")) + } + } + + name.push_str("! :-( "); + } + name.push_str(&format!("{} ", self.name)); + + if let Some(triptype) = tripdetails.triptype(db).await { + name.push_str(&format!("• {} ", triptype.name)) + } + vevent.push(Summary::new(name)); + vevent + } + pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails { TripDetails::find_by_id(db, self.trip_details_id) .await diff --git a/src/model/family.rs b/src/model/family.rs index f648cfd..894b082 100644 --- a/src/model/family.rs +++ b/src/model/family.rs @@ -75,7 +75,7 @@ GROUP BY family.id;" } pub async fn members(&self, db: &SqlitePool) -> Vec { - sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id) + sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id) .fetch_all(db) .await .unwrap() diff --git a/src/model/personal/cal.rs b/src/model/personal/cal.rs new file mode 100644 index 0000000..88e6b7e --- /dev/null +++ b/src/model/personal/cal.rs @@ -0,0 +1,27 @@ +use std::io::Write; + +use ics::{components::Property, ICalendar}; +use sqlx::SqlitePool; + +use crate::model::{event::Event, trip::Trip, user::User}; + +pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { + let mut calendar = ICalendar::new("2.0", "ics-rs"); + calendar.push(Property::new( + "X-WR-CALNAME", + "Donau Linz - Deine Ausfahrten", + )); + + let events = Event::all_with_user(db, user).await; + for event in events { + calendar.add_event(event.get_vevent(db).await); + } + + let trips = Trip::all_with_user(db, user).await; + for trip in trips { + calendar.add_event(trip.get_vevent(db).await); + } + let mut buf = Vec::new(); + write!(&mut buf, "{}", calendar).unwrap(); + String::from_utf8(buf).unwrap() +} diff --git a/src/model/personal/mod.rs b/src/model/personal/mod.rs index cc9c53c..40a93c9 100644 --- a/src/model/personal/mod.rs +++ b/src/model/personal/mod.rs @@ -5,6 +5,7 @@ use sqlx::SqlitePool; use super::{logbook::Logbook, stat::Stat, user::User}; +pub(crate) mod cal; pub(crate) mod equatorprice; pub(crate) mod rowingbadge; diff --git a/src/model/rower.rs b/src/model/rower.rs index 7099cff..fd477b2 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -16,7 +16,7 @@ impl Rower { sqlx::query_as!( User, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) ", diff --git a/src/model/trip.rs b/src/model/trip.rs index fabc4de..46a97ca 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -1,4 +1,5 @@ use chrono::{Local, NaiveDate}; +use ics::properties::{DtStart, Summary}; use serde::Serialize; use sqlx::SqlitePool; @@ -9,6 +10,7 @@ use super::{ tripdetails::TripDetails, triptype::TripType, user::{CoxUser, User}, + usertrip::UserTrip, }; #[derive(Serialize, Clone, Debug)] @@ -123,6 +125,61 @@ WHERE trip_details.id=? .ok() } + pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { + let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000"); + vevent.push(DtStart::new(format!( + "{}T{}00", + self.day.replace('-', ""), + self.planned_starting_time.replace(':', "") + ))); + let mut name = String::new(); + if self.is_cancelled() { + name.push_str("ABGESAGT"); + if let Some(notes) = &self.notes { + if !notes.is_empty() { + name.push_str(&format!(" (Grund: {notes})")) + } + } + + name.push_str("! :-( "); + } + name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name)); + + vevent.push(Summary::new(name)); + vevent + } + + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!( + Self, + " +SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked +FROM trip +INNER JOIN trip_details ON trip.trip_details_id = trip_details.id +INNER JOIN user ON trip.cox_id = user.id +", + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + + pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec { + let mut ret = Vec::new(); + let trips = Self::all(db).await; + for trip in trips { + if let Some(trip_details_id) = trip.trip_details_id { + if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id) + .await + .is_some() + { + ret.push(trip); + } + } + } + ret + } + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, @@ -370,6 +427,10 @@ WHERE day=? trips.retain(|e| e.trip.always_show); trips } + + fn is_cancelled(&self) -> bool { + self.max_people == 0 + } } #[derive(Debug)] diff --git a/src/model/user.rs b/src/model/user.rs index d5f8a41..717596b 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -42,6 +42,7 @@ pub struct User { pub phone: Option, pub address: Option, pub family_id: Option, + pub user_token: String, } #[derive(Debug, Serialize, Deserialize)] @@ -493,7 +494,7 @@ ASKÖ Ruderverein Donau Linz", self.name), sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id like ? ", @@ -508,7 +509,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id like ? ", @@ -525,7 +526,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE lower(name)=? ", @@ -567,7 +568,7 @@ WHERE lower(name)=? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -589,7 +590,7 @@ ORDER BY last_access DESC sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user u JOIN user_role ur ON u.id = ur.user_id WHERE ur.role_id = ? AND deleted = 0 @@ -605,14 +606,14 @@ ORDER BY name; sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id IS NOT NULL GROUP BY family_id UNION -- Select users with a null family_id, without grouping -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id IS NULL; " ) @@ -625,7 +626,7 @@ WHERE family_id IS NULL; sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' ORDER BY name @@ -640,7 +641,7 @@ ORDER BY name sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 ORDER BY last_access DESC diff --git a/src/tera/misc.rs b/src/tera/misc.rs index 5cf48ff..4d0e76e 100644 --- a/src/tera/misc.rs +++ b/src/tera/misc.rs @@ -1,7 +1,7 @@ use rocket::{get, http::ContentType, routes, Route, State}; use sqlx::SqlitePool; -use crate::model::event::Event; +use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; #[get("/cal")] async fn cal(db: &State) -> (ContentType, String) { @@ -9,8 +9,25 @@ async fn cal(db: &State) -> (ContentType, String) { (ContentType::Calendar, Event::get_ics_feed(db).await) } +#[get("/cal/personal//")] +async fn cal_registered( + db: &State, + user_id: i32, + uuid: &str, +) -> Result<(ContentType, String), String> { + let Some(user) = User::find_by_id(db, user_id).await else { + return Err("Invalid".into()); + }; + + if &user.user_token != uuid { + return Err("Invalid".into()); + } + + Ok((ContentType::Calendar, get_personal_cal(db, &user).await)) +} + pub fn routes() -> Vec { - routes![cal] + routes![cal, cal_registered] } #[cfg(test)] diff --git a/templates/index.html.tera b/templates/index.html.tera index c717c89..0d8200b 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -103,7 +103,7 @@ +
+

+

+ +  📅  Kalender + +

+ Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an: +

+
    +
  1. + Alle Events und Ausfahrten, zu denen du dich angemeldet hast: https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }} +
    + Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast. +
  2. +
  3. + Allgemeiner Kalender, zB save-the-dates (Wanderfahrten, ...): https://rudernlinz.at/cal +
  4. +
  5. + Alle Events: https://app.rudernlinz.at/cal +
    + Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf https://rudernlinz.at/termine verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken. +
  6. +
+ Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab. +
+

+