diff --git a/src/model/boat.rs b/src/model/boat.rs index 244bc7f..6fbd59a 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -78,6 +78,18 @@ impl Boat { .ok() } + pub async fn shipmaster_allowed(&self, user: &User) -> bool { + if let Some(owner_id) = self.owner { + return owner_id == user.id; + } + + if self.amount_seats == 1 { + return true; + } + + user.is_cox + } + pub async fn is_locked(&self, db: &SqlitePool) -> bool { sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some() } @@ -136,7 +148,7 @@ ORDER BY amount_seats DESC if user.is_admin { return Self::all(db).await; } - let mut boats; + let boats; if user.is_cox { boats = sqlx::query_as!( Boat, diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 377cf36..735aecc 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -41,7 +41,7 @@ pub struct LogToAdd { pub rowers: Vec, } -#[derive(FromForm)] +#[derive(FromForm, Debug)] pub struct LogToFinalize { pub destination: String, pub distance_in_km: i64, @@ -75,6 +75,7 @@ pub enum LogbookDeleteError { #[derive(Debug, PartialEq)] pub enum LogbookCreateError { ArrivalSetButNoDestination, + UserNotAllowedToUseBoat, ArrivalSetButNoDistance, BoatAlreadyOnWater, BoatLocked, @@ -170,7 +171,11 @@ ORDER BY departure DESC ret } - pub async fn create(db: &SqlitePool, log: LogToAdd) -> Result<(), LogbookCreateError> { + pub async fn create( + db: &SqlitePool, + log: LogToAdd, + created_by_user: &User, + ) -> Result<(), LogbookCreateError> { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { return Err(LogbookCreateError::BoatNotFound); }; @@ -183,10 +188,9 @@ ORDER BY departure DESC return Err(LogbookCreateError::BoatAlreadyOnWater); } - if (User::find_by_id(db, log.shipmaster as i32).await.unwrap()) - .on_water(db) - .await - { + let shipmaster = User::find_by_id(db, log.shipmaster as i32).await.unwrap(); + + if shipmaster.on_water(db).await { return Err(LogbookCreateError::ShipmasterAlreadyOnWater); } @@ -223,6 +227,10 @@ ORDER BY departure DESC } } + if !boat.shipmaster_allowed(created_by_user).await { + return Err(LogbookCreateError::UserNotAllowedToUseBoat); + } + //let departure = format!("{}+02:00", &log.departure); let mut tx = db.begin().await.unwrap(); @@ -431,7 +439,7 @@ mod test { &pool, LogToAdd { boat_id: 3, - shipmaster: 5, + shipmaster: 4, shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, @@ -441,6 +449,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 4).await.unwrap(), ) .await .unwrap() @@ -464,6 +473,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 4).await.unwrap(), ) .await; @@ -488,6 +498,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 4).await.unwrap(), ) .await; @@ -512,6 +523,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 5).await.unwrap(), ) .await; @@ -536,6 +548,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 5).await.unwrap(), ) .await; @@ -560,6 +573,7 @@ mod test { logtype: None, rowers: Vec::new(), }, + &User::find_by_id(&pool, 2).await.unwrap(), ) .await; @@ -584,6 +598,7 @@ mod test { logtype: None, rowers: vec![5], }, + &User::find_by_id(&pool, 5).await.unwrap(), ) .await; @@ -608,6 +623,7 @@ mod test { logtype: None, rowers: vec![1], }, + &User::find_by_id(&pool, 5).await.unwrap(), ) .await; diff --git a/src/tera/log.rs b/src/tera/log.rs index acf3e31..6c6442a 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -20,7 +20,7 @@ use crate::model::{ LogbookUpdateError, }, logtype::LogType, - user::{AdminUser, User, UserWithWaterStatus}, + user::{User, UserWithWaterStatus}, }; pub struct KioskCookie(String); @@ -139,10 +139,11 @@ async fn kiosk( Template::render("kiosk", context.into_json()) } -async fn create_logbook(db: &SqlitePool, data: Form) -> Flash { +async fn create_logbook(db: &SqlitePool, data: Form, user: &User) -> Flash { match Logbook::create( db, - data.into_inner() + data.into_inner(), + user ) .await { @@ -158,17 +159,14 @@ async fn create_logbook(db: &SqlitePool, data: Form) -> Flash Flash::error(Redirect::to("/log"), format!("Distanz notwendig, wenn Ankunftszeit angegeben wurde")), Err(LogbookCreateError::ArrivalSetButNoDestination) => Flash::error(Redirect::to("/log"), format!("Ziel notwendig, wenn Ankunftszeit angegeben wurde")), Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), format!("Ankunftszeit kann nicht vor der Abfahrtszeit sein")), + Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), format!("Schiffsführer darf dieses Boot nicht verwenden")), } } #[post("/", data = "", rank = 2)] -async fn create( - db: &State, - data: Form, - _adminuser: AdminUser, -) -> Flash { - create_logbook(db, data).await +async fn create(db: &State, data: Form, user: User) -> Flash { + create_logbook(db, data, &user).await } #[post("/", data = "")] @@ -177,7 +175,8 @@ async fn create_kiosk( data: Form, _kiosk: KioskCookie, ) -> Flash { - create_logbook(db, data).await + let creator = User::find_by_id(db, data.shipmaster as i32).await.unwrap(); + create_logbook(db, data, &creator).await } async fn home_logbook( @@ -498,6 +497,11 @@ mod test { let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + let mut client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Linz"); let _ = req.dispatch().await; @@ -517,11 +521,81 @@ mod test { &db, &mut client, "second_private_boat_from_rower".into(), - "admin".into(), + "rower".into(), ) .await; } + #[sqlx::test] + fn test_shipowner_can_allow_others_to_drive() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + // Owner can start trip: + let boat_id = Boat::find_by_name(&db, "private_boat_from_rower".into()) + .await + .unwrap() + .id; + let shipmaster_id = User::find_by_name(&db, "rower2".into()).await.unwrap().id; + + let req = client.post("/log").header(ContentType::Form).body(format!( + "boat_id={boat_id}&shipmaster={shipmaster_id}&departure=1199-12-31T10:00" + )); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt erfolgreich hinzugefügt" + ); + + // Shipmaster can end it + let log_id = Logbook::highest_id(&db).await; + + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower2&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post(format!("/log/{log_id}")) + .header(ContentType::Form) + .body("destination=Ottensheim&distance_in_km=25"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successSuccessfully updated log"); + } + #[sqlx::test] fn test_normal_user_sees_appropriate_boats() { let db = testdb!(); @@ -529,7 +603,7 @@ mod test { let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); - let client = Client::tracked(rocket).await.unwrap(); + let mut client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form @@ -541,16 +615,176 @@ mod test { let text = response.into_string().await.unwrap(); + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + //Sees all 1x assert!(text.contains("Haichenbach")); + can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "rower".into()).await; + assert!(text.contains("private_boat_from_rower")); + can_start_and_end_trip( + &db, + &mut client, + "private_boat_from_rower".into(), + "rower".into(), + ) + .await; + assert!(text.contains("second_private_boat_from_rower")); + can_start_and_end_trip( + &db, + &mut client, + "second_private_boat_from_rower".into(), + "rower".into(), + ) + .await; //Don't see anything else assert!(!text.contains("Joe")); + cant_start_trip( + &db, + &mut client, + "Joe".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + assert!(!text.contains("Kaputtes Boot :-(")); + cant_start_trip( + &db, + &mut client, + "Kaputtes Boot :-(".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + assert!(!text.contains("Sehr kaputtes Boot :-((")); + cant_start_trip( + &db, + &mut client, + "Sehr kaputtes Boot :-((".into(), + "rower".into(), + "Boot gesperrt".into(), + ) + .await; + assert!(!text.contains("Ottensheim Boot")); + cant_start_trip( + &db, + &mut client, + "Ottensheim Boot".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + } + + #[sqlx::test] + fn test_cox_sees_appropriate_boats() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let mut client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=cox&password=cox"); // Add the form data to the request body; + login.dispatch().await; + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + + //Sees all 1x + assert!(text.contains("Haichenbach")); + can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "cox".into()).await; + + assert!(text.contains("Joe")); + can_start_and_end_trip(&db, &mut client, "Joe".into(), "cox".into()).await; + + assert!(text.contains("Kaputtes Boot :-(")); + can_start_and_end_trip(&db, &mut client, "Kaputtes Boot :-(".into(), "cox".into()).await; + + assert!(text.contains("Sehr kaputtes Boot :-((")); + cant_start_trip( + &db, + &mut client, + "Sehr kaputtes Boot :-((".into(), + "cox".into(), + "Boot gesperrt".into(), + ) + .await; + + assert!(text.contains("Ottensheim Boot")); + can_start_and_end_trip(&db, &mut client, "Ottensheim Boot".into(), "cox".into()).await; + + //Can't use private boats + assert!(!text.contains("private_boat_from_rower")); + cant_start_trip( + &db, + &mut client, + "private_boat_from_rower".into(), + "cox".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + + assert!(!text.contains("second_private_boat_from_rower")); + cant_start_trip( + &db, + &mut client, + "second_private_boat_from_rower".into(), + "cox".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + } + + #[sqlx::test] + fn test_cant_end_trip_other_user() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower2&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/log/1") + .header(ContentType::Form) + .body("destination=Ottensheim&distance_in_km=25"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorLogbook with ID 1 could not be updated!" + ); } async fn can_start_and_end_trip( @@ -559,7 +793,6 @@ mod test { boat_name: String, shipmaster_name: String, ) { - println!("{boat_name}"); let boat_id = Boat::find_by_name(db, boat_name).await.unwrap().id; let shipmaster_id = User::find_by_name(db, &shipmaster_name).await.unwrap().id; @@ -576,7 +809,6 @@ mod test { .get("_flash") .expect("Expected flash cookie"); - println!("{shipmaster_id}"); assert_eq!( flash_cookie.value(), "7:successAusfahrt erfolgreich hinzugefügt" @@ -599,14 +831,6 @@ mod test { .expect("Expected flash cookie"); assert_eq!(flash_cookie.value(), "7:successSuccessfully updated log"); - - //TODO: Remove the following query? - //sqlx::query(&format!( - // "DELETE FROM logbook WHERE boat_id={boat_id} AND shipmaster={shipmaster_id}" - //)) - //.execute(db) - //.await - //.unwrap(); } async fn cant_start_trip(