From cedaba570925df9c3de87916b15b195dfde0dddf Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 4 Apr 2023 15:16:21 +0200 Subject: [PATCH] push --- README.md | 16 +----- migration.sql | 21 +++++++ src/model/mod.rs | 11 +++- src/model/planned_event.rs | 86 +++++++++++++++++++++++++++-- src/model/trip.rs | 109 +++++++++++++++++++++++++++++++++++++ src/model/user.rs | 45 +++++++++++++++ src/model/usertrip.rs | 27 +++++++++ src/rest/cox.rs | 58 ++++++++++++++++++++ src/rest/mod.rs | 51 ++++++++++++++--- templates/index.html.tera | 57 +++++++++++++++++++ 10 files changed, 453 insertions(+), 28 deletions(-) create mode 100644 src/model/trip.rs create mode 100644 src/model/usertrip.rs create mode 100644 src/rest/cox.rs diff --git a/README.md b/README.md index 79246f6..7744f6a 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,6 @@ - Link for specific trip - Basic auth (with e.g. ekrv) to prevent spam bots? (Or on first login there are 2 input fields: name + e.g. name of "strom") - -# DB - -- trip - - id: i32 - - cox_id: i32 (user.id) - - trip_details: Option (trip_details.id) - - planned_event_id: Option (planned_event.id) - - created: chrono::DateTime -- user_trip - - trip_details_id: i32 (trip_details.id) - - user_id: i32 (user.id) - - created: chrono::DateTime - # TODO - [x] User login - [x] Admin @@ -37,5 +23,5 @@ - [ ] Ausfahrten - [x] CRUD planned_event - [x] CRUD trip_details - - [ ] CRUD trip + - [x] CRUD trip - [ ] CRUD user_trip diff --git a/migration.sql b/migration.sql index 043e83f..55d2dd0 100644 --- a/migration.sql +++ b/migration.sql @@ -24,3 +24,24 @@ CREATE TABLE IF NOT EXISTS "planned_event" ( "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(trip_details_id) REFERENCES trip_details(id) ON DELETE CASCADE ); + +CREATE TABLE IF NOT EXISTS "trip" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "cox_id" INTEGER NOT NULL, + "trip_details_id" INTEGER, + "planned_event_id" INTEGER, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(cox_id) REFERENCES user(id), + FOREIGN KEY(trip_details_id) REFERENCES trip_details(id) ON DELETE CASCADE, + FOREIGN KEY(planned_event_id) REFERENCES planned_event(id) ON DELETE CASCADE, + CONSTRAINT unq UNIQUE (cox_id, planned_event_id) -- allow cox to participate only once for each planned event +); + +CREATE TABLE IF NOT EXISTS "user_trip" ( + "user_id" INTEGER NOT NULL, + "trip_details_id" INTEGER NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES user(id), + FOREIGN KEY(trip_details_id) REFERENCES trip_details(id), + CONSTRAINT unq UNIQUE (user_id, trip_details_id) -- allow user to participate only once for each trip +); diff --git a/src/model/mod.rs b/src/model/mod.rs index cfb6082..c7742b8 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -2,17 +2,23 @@ use chrono::NaiveDate; use serde::Serialize; use sqlx::SqlitePool; -use self::planned_event::PlannedEvent; +use self::{ + planned_event::{PlannedEvent, PlannedEventWithUser}, + trip::{Trip, TripWithUser}, +}; pub mod planned_event; +pub mod trip; pub mod tripdetails; pub mod user; +pub mod usertrip; //pub mod users; #[derive(Serialize)] pub struct Day { day: NaiveDate, - planned_events: Vec, + planned_events: Vec, + trips: Vec, } impl Day { @@ -20,6 +26,7 @@ impl Day { Self { day, planned_events: PlannedEvent::get_for_day(db, day).await, + trips: Trip::get_for_day(db, day).await, } } } diff --git a/src/model/planned_event.rs b/src/model/planned_event.rs index e11d8ff..7b08ae2 100644 --- a/src/model/planned_event.rs +++ b/src/model/planned_event.rs @@ -2,7 +2,7 @@ use chrono::NaiveDate; use serde::Serialize; use sqlx::SqlitePool; -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct PlannedEvent { id: i64, name: String, @@ -15,10 +15,18 @@ pub struct PlannedEvent { notes: Option, } +#[derive(Serialize)] +pub struct PlannedEventWithUser { + #[serde(flatten)] + planned_event: PlannedEvent, + cox: Vec, + rower: Vec, +} + impl PlannedEvent { - pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { + pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { let day = format!("{}", day); - sqlx::query_as!( + let events = sqlx::query_as!( PlannedEvent, " SELECT planned_event.id, name, planned_amount_cox, allow_guests, trip_details_id, planned_starting_time, max_people, day, notes @@ -30,7 +38,77 @@ WHERE day=? ) .fetch_all(db) .await - .unwrap() //TODO: fixme + .unwrap(); //TODO: fixme + + let mut ret = Vec::new(); + for event in events { + ret.push(PlannedEventWithUser { + planned_event: event.clone(), + cox: Self::get_all_cox_for_id(db, event.id).await, + rower: Self::get_all_rower_for_id(db, event.id).await, + }) + } + ret + } + + pub async fn rower_can_register(db: &SqlitePool, trip_details_id: i64) -> bool { + let amount_currently_registered = sqlx::query!( + " + SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ? + ", + trip_details_id + ) + .fetch_one(db) + .await + .unwrap(); //TODO: fixme + let amount_currently_registered = amount_currently_registered.count as i64; + + let amount_allowed_to_register = sqlx::query!( + " + SELECT max_people FROM trip_details WHERE id = ? + ", + trip_details_id + ) + .fetch_one(db) + .await + .unwrap(); //TODO: fixme + let amount_allowed_to_register = amount_allowed_to_register.max_people; + + amount_currently_registered < amount_allowed_to_register + } + + async fn get_all_cox_for_id(db: &SqlitePool, id: i64) -> Vec { + let res = sqlx::query!( + " +SELECT (SELECT name FROM user WHERE cox_id = id) as name FROM trip WHERE planned_event_id = ? + ", + id + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + let mut ret = Vec::new(); + for r in res { + ret.push(r.name); + } + ret + } + + async fn get_all_rower_for_id(db: &SqlitePool, id: i64) -> Vec { + let res = sqlx::query!( + " +SELECT (SELECT name FROM user WHERE user_trip.user_id = user.id) as name FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM planned_event WHERE id = ?) + ", + id + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + let mut ret = Vec::new(); + for r in res { + ret.push(r.name); + } + ret } pub async fn new( diff --git a/src/model/trip.rs b/src/model/trip.rs new file mode 100644 index 0000000..7e9245b --- /dev/null +++ b/src/model/trip.rs @@ -0,0 +1,109 @@ +use chrono::NaiveDate; +use serde::Serialize; +use sqlx::SqlitePool; + +#[derive(Serialize, Clone)] +pub struct Trip { + id: i64, + cox_id: i64, + cox_name: String, + trip_details_id: Option, + planned_starting_time: String, + max_people: i64, + day: String, + notes: Option, +} + +#[derive(Serialize)] +pub struct TripWithUser { + #[serde(flatten)] + trip: Trip, + rower: Vec, +} + +impl Trip { + pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { + let day = format!("{}", day); + let trips = sqlx::query_as!( + Trip, + " +SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes +FROM trip +INNER JOIN trip_details ON trip.trip_details_id = trip_details.id +INNER JOIN user ON trip.cox_id = user.id +WHERE day=? + ", + day + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + let mut ret = Vec::new(); + for trip in trips { + ret.push(TripWithUser { + trip: trip.clone(), + rower: Self::get_all_rower_for_id(db, trip.id).await, + }) + } + ret + } + + async fn get_all_rower_for_id(db: &SqlitePool, id: i64) -> Vec { + let res = sqlx::query!( + " +SELECT (SELECT name FROM user WHERE user_trip.user_id = user.id) as name FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE id = ?) + ", + id + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + let mut ret = Vec::new(); + for r in res { + ret.push(r.name); + } + ret + } + + pub async fn new_own(db: &SqlitePool, cox_id: i64, trip_details_id: i64) { + sqlx::query!( + "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", + cox_id, + trip_details_id + ) + .execute(db) + .await + .unwrap(); //TODO: fixme + } + + /// Returns true if successfully inserted; false if not (e.g. because user is already + /// participant + pub async fn new_join(db: &SqlitePool, cox_id: i64, planned_event_id: i64) -> bool { + sqlx::query!( + "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", + cox_id, + planned_event_id + ) + .execute(db) + .await + .is_ok() + } + + pub async fn delete(db: &SqlitePool, user_id: i64, planned_event_id: i64) { + let _ = sqlx::query!( + "DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?", + user_id, + planned_event_id + ) + .execute(db) + .await + .is_ok(); + } + + //pub async fn delete(db: &SqlitePool, id: i64) { + // sqlx::query!("DELETE FROM planned_event WHERE id = ?", id) + // .execute(db) + // .await + // .unwrap(); //TODO: fixme + //} +} diff --git a/src/model/user.rs b/src/model/user.rs index 46d0b59..f15e2a4 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use rocket::{ async_trait, @@ -34,12 +36,37 @@ impl TryFrom for AdminUser { } } +pub struct CoxUser { + user: User, +} + +impl Deref for CoxUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.user + } +} + +impl TryFrom for CoxUser { + type Error = LoginError; + + fn try_from(user: User) -> Result { + if user.is_cox { + Ok(CoxUser { user }) + } else { + Err(LoginError::NotACox) + } + } +} + #[derive(Debug)] pub enum LoginError { SqlxError(sqlx::Error), InvalidAuthenticationCombo, NotLoggedIn, NotAnAdmin, + NotACox, NoPasswordSet(User), } @@ -181,6 +208,24 @@ impl<'r> FromRequest<'r> for AdminUser { } } +#[async_trait] +impl<'r> FromRequest<'r> for CoxUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + match req.cookies().get_private("loggedin_user") { + Some(user) => { + let user: User = serde_json::from_str(&user.value()).unwrap(); //TODO: fixme + match user.try_into() { + Ok(user) => Outcome::Success(user), + Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotAnAdmin)), + } + } + None => Outcome::Failure((Status::Unauthorized, LoginError::NotLoggedIn)), + } + } +} + #[cfg(test)] mod test { use crate::testdb; diff --git a/src/model/usertrip.rs b/src/model/usertrip.rs new file mode 100644 index 0000000..dec0ff3 --- /dev/null +++ b/src/model/usertrip.rs @@ -0,0 +1,27 @@ +use sqlx::SqlitePool; + +pub struct UserTrip {} + +impl UserTrip { + pub async fn new(db: &SqlitePool, user_id: i64, trip_details_id: i64) -> bool { + sqlx::query!( + "INSERT INTO user_trip (user_id, trip_details_id) VALUES(?, ?)", + user_id, + trip_details_id + ) + .execute(db) + .await + .is_ok() + } + + pub async fn delete(db: &SqlitePool, user_id: i64, trip_details_id: i64) { + let _ = sqlx::query!( + "DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?", + user_id, + trip_details_id + ) + .execute(db) + .await + .is_ok(); + } +} diff --git a/src/rest/cox.rs b/src/rest/cox.rs new file mode 100644 index 0000000..5ac43f3 --- /dev/null +++ b/src/rest/cox.rs @@ -0,0 +1,58 @@ +use rocket::{ + form::Form, + get, post, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use sqlx::SqlitePool; + +use crate::model::{trip::Trip, tripdetails::TripDetails, user::CoxUser}; + +//TODO: add constraints (e.g. planned_amount_cox > 0) +#[derive(FromForm)] +struct AddTripForm { + day: String, + planned_starting_time: String, + max_people: i32, + notes: Option, +} + +#[post("/trip", data = "")] +async fn create(db: &State, data: Form, cox: CoxUser) -> Flash { + //TODO: fix clones() + let trip_details_id = TripDetails::new( + db, + data.planned_starting_time.clone(), + data.max_people, + data.day.clone(), + data.notes.clone(), + ) + .await; + + //TODO: fix clone() + Trip::new_own(db, cox.id, trip_details_id).await; + + Flash::success(Redirect::to("/"), "Successfully planned the event") +} + +#[get("/join/")] +async fn join(db: &State, planned_event_id: i64, cox: CoxUser) -> Flash { + if Trip::new_join(db, cox.id, planned_event_id).await { + Flash::success(Redirect::to("/"), "Danke für's helfen!") + } else { + Flash::error(Redirect::to("/"), "Du nimmst bereits teil!") + } +} + +#[get("/remove/")] +async fn remove(db: &State, planned_event_id: i64, cox: CoxUser) -> Flash { + //TODO: Check if > 2 hrs to event + + Trip::delete(db, cox.id, planned_event_id).await; + + Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") +} + +pub fn routes() -> Vec { + routes![create, join, remove] +} diff --git a/src/rest/mod.rs b/src/rest/mod.rs index 6dbc05c..0cd574e 100644 --- a/src/rest/mod.rs +++ b/src/rest/mod.rs @@ -1,21 +1,57 @@ -use chrono::{Duration, Local, NaiveDate}; -use rocket::{catch, catchers, get, response::Redirect, routes, Build, Rocket, State}; -use rocket_dyn_templates::{context, Template}; +use chrono::{Duration, Local}; +use rocket::{ + catch, catchers, get, + request::FlashMessage, + response::{Flash, Redirect}, + routes, Build, Rocket, State, +}; +use rocket_dyn_templates::{context, tera::Context, Template}; use sqlx::SqlitePool; -use crate::model::{user::User, Day}; +use crate::model::{planned_event::PlannedEvent, user::User, usertrip::UserTrip, Day}; mod admin; mod auth; +mod cox; #[get("/")] -async fn index(db: &State, user: User) -> Template { +async fn index(db: &State, user: User, flash: Option>) -> Template { let mut days = Vec::new(); for i in 0..6 { let date = (Local::now() + Duration::days(i)).date_naive(); days.push(Day::new(db, date).await); } - Template::render("index", context! {loggedin_user: user, days}) + + let mut context = Context::new(); + + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &user); + context.insert("days", &days); + Template::render("index", context.into_json()) +} + +#[get("/join/")] +async fn join(db: &State, trip_details_id: i64, user: User) -> Flash { + if !PlannedEvent::rower_can_register(db, trip_details_id).await { + return Flash::error(Redirect::to("/"), "Bereits ausgebucht!"); + } + + if UserTrip::new(db, user.id, trip_details_id).await { + Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!") + } else { + Flash::error(Redirect::to("/"), "Du nimmst bereits teil!") + } +} + +#[get("/remove/")] +async fn remove(db: &State, trip_details_id: i64, user: User) -> Flash { + //TODO: Check if > 2 hrs to event + + UserTrip::delete(db, user.id, trip_details_id).await; + + Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") } #[catch(401)] //unauthorized @@ -26,8 +62,9 @@ fn unauthorized_error() -> Redirect { pub fn start(db: SqlitePool) -> Rocket { rocket::build() .manage(db) - .mount("/", routes![index]) + .mount("/", routes![index, join, remove]) .mount("/auth", auth::routes()) + .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) .register("/", catchers![unauthorized_error]) .attach(Template::fairing()) diff --git a/templates/index.html.tera b/templates/index.html.tera index e7813c2..6007001 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -39,12 +39,56 @@ Planned starting time: {{ planned_event.planned_starting_time }}
Max people: {{ planned_event.max_people }}
Notes: {{ planned_event.notes }}
+ Folgende Steuerpersonen haben sich schon angemeldet: + {% for cox in planned_event.cox %} + {{ cox }} + {% if cox == loggedin_user.name %} + ABMELDEN + {% endif %} + {% endfor %} +
+ + Folgende Ruderer haben sich schon angemeldet: + {% for rower in planned_event.rower%} + {{ rower }} + {% if rower == loggedin_user.name %} + ABMELDEN + {% endif %} + {% endfor %} + + {% if planned_event.max_people > planned_event.rower | length %} + MITRUDERN + {% endif %} + + + {% if loggedin_user.is_cox %} + STEUERN + {% endif %} {% if loggedin_user.is_admin %} DELETE {% endif %} {% endfor %} + + {% for trip in day.trips %} +

Ausfahrt von {{ trip.cox_name }}

+ Planned starting time: {{ trip.planned_starting_time }}
+ Max people: {{ trip.max_people }}
+ Notes: {{ trip.notes }}
+ Folgende Ruderer haben sich schon angemeldet: + {% for rower in trip.rower %} + {{ rower }} + {% if rower == loggedin_user.name %} + ABMELDEN + {% endif %} + {% endfor %} + + {% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id %} + MITRUDERN + {% endif %} + {% endfor %} + {% if loggedin_user.is_admin %}

Add planned event

@@ -60,6 +104,19 @@
{% endif %} + {% if loggedin_user.is_cox%} +

Add trip

+
+ + + + + + +
+ {% endif %} + +
{% endfor %}