diff --git a/seeds.sql b/seeds.sql index 9acfe7f..ba7b50f 100644 --- a/seeds.sql +++ b/seeds.sql @@ -2,18 +2,26 @@ INSERT INTO "role" (name) VALUES ('admin'); INSERT INTO "role" (name) VALUES ('cox'); INSERT INTO "role" (name) VALUES ('scheckbuch'); INSERT INTO "role" (name) VALUES ('tech'); +INSERT INTO "role" (name) VALUES ('Donau Linz'); INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); +INSERT INTO "user_role" (user_id, role_id) VALUES(1,5); INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user_role" (user_id, role_id) VALUES(2,5); INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); +INSERT INTO "user_role" (user_id, role_id) VALUES(3,5); INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(4,5); INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); INSERT INTO "user" (name) VALUES('new'); +INSERT INTO "user_role" (user_id, role_id) VALUES(5,5); INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(6,5); INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user_role" (user_id, role_id) VALUES(7,5); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 98d6c87..dfb8278 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -264,6 +264,10 @@ ORDER BY departure DESC return Err(LogbookCreateError::BoatNotFound); }; + if boat.amount_seats == 1 && log.rowers.is_empty() { + log.rowers = vec![created_by_user.id]; + } + if boat.amount_seats == 1 { log.shipmaster = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]); diff --git a/src/model/user.rs b/src/model/user.rs index 3fc4943..eb7edbd 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -440,23 +440,20 @@ impl<'r> FromRequest<'r> for User { Ok(user_id) => { let db = req.rocket().state::().unwrap(); let Some(user) = User::find_by_id(db, user_id).await else { - return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound)); + return Outcome::Error((Status::Forbidden, LoginError::UserNotFound)); }; if user.deleted { - return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted)); + return Outcome::Error((Status::Forbidden, LoginError::UserDeleted)); } user.logged_in(db).await; let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); - cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); + cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2)); req.cookies().add_private(cookie); Outcome::Success(user) } - Err(_) => { - println!("{:?}", user_id.value()); - Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)) - } + Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)), }, None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), } @@ -487,7 +484,7 @@ impl<'r> FromRequest<'r> for TechUser { if user.has_role(db, "tech").await { Outcome::Success(TechUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -530,7 +527,7 @@ impl<'r> FromRequest<'r> for CoxUser { if user.has_role(db, "cox").await { Outcome::Success(CoxUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -555,7 +552,7 @@ impl<'r> FromRequest<'r> for AdminUser { if user.has_role(db, "admin").await { Outcome::Success(AdminUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -565,22 +562,65 @@ impl<'r> FromRequest<'r> for AdminUser { } #[derive(Debug, Serialize, Deserialize)] -pub struct NonGuestUser { - pub(crate) user: User, -} +pub struct AllowedForPlannedTripsUser(pub(crate) User); #[async_trait] -impl<'r> FromRequest<'r> for NonGuestUser { +impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { - if !user.has_role(db, "scheckbuch").await { - Outcome::Success(NonGuestUser { user }) + if user.has_role(db, "Donau Linz").await { + Outcome::Success(AllowedForPlannedTripsUser(user)) + } else if user.has_role(db, "scheckbuch").await { + Outcome::Success(AllowedForPlannedTripsUser(user)) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) + } + } + Outcome::Error(f) => Outcome::Error(f), + Outcome::Forward(f) => Outcome::Forward(f), + } + } +} + +impl Into for AllowedForPlannedTripsUser { + fn into(self) -> User { + self.0 + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DonauLinzUser(pub(crate) User); + +impl Into for DonauLinzUser { + fn into(self) -> User { + self.0 + } +} + +impl Deref for DonauLinzUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for DonauLinzUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { + Outcome::Success(user) => { + if user.has_role(db, "Donau Linz").await { + Outcome::Success(DonauLinzUser(user)) + } else { + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs index e45c260..aa3af59 100644 --- a/src/tera/admin/mail.rs +++ b/src/tera/admin/mail.rs @@ -50,9 +50,9 @@ async fn update( ) -> Flash { let d = data.into_inner(); if Mail::send(db, d, config.smtp_pw.clone()).await { - return Flash::success(Redirect::to("/admin/mail"), "Mail versendet"); + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") } else { - return Flash::error(Redirect::to("/admin/mail"), "Fehler"); + Flash::error(Redirect::to("/admin/mail"), "Fehler") } } diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs index c798791..39088af 100644 --- a/src/tera/boatdamage.rs +++ b/src/tera/boatdamage.rs @@ -13,7 +13,7 @@ use crate::{ model::{ boat::Boat, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, - user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, + user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, }, tera::log::KioskCookie, }; @@ -45,7 +45,7 @@ async fn index_kiosk( async fn index( db: &State, flash: Option>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Template { let boatdamages = BoatDamage::all(db).await; let boats = Boat::all(db).await; @@ -59,7 +59,7 @@ async fn index( context.insert("boats", &boats); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.user, db).await, + &UserWithRoles::from_user(user.into(), db).await, ); Template::render("boatdamages", context.into_json()) @@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> { async fn create<'r>( db: &State, data: Form>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { + let user: User = user.into(); let boatdamage_to_add = BoatDamageToAdd { boat_id: data.boat_id, desc: data.desc, lock_boat: data.lock_boat, - user_id_created: user.user.id as i32, + user_id_created: user.id as i32, }; match BoatDamage::create(db, boatdamage_to_add).await { Ok(_) => Flash::success( diff --git a/src/tera/cox.rs b/src/tera/cox.rs index a62e2b7..808f842 100644 --- a/src/tera/cox.rs +++ b/src/tera/cox.rs @@ -391,7 +391,7 @@ mod test { .body("name=cox&password=cox"); // Add the form data to the request body; login.dispatch().await; - let req = client.get("/join/1"); + let req = client.get("/planned/join/1"); let _ = req.dispatch().await; let req = client.get("/cox/join/1"); diff --git a/src/tera/log.rs b/src/tera/log.rs index 566c874..0d480e0 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -23,7 +23,7 @@ use crate::model::{ LogbookUpdateError, }, logtype::LogType, - user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus}, + user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus}, }; pub struct KioskCookie(String); @@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie { async fn index( db: &State, flash: Option>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Template { - let boats = Boat::for_user(db, &user.user).await; + let boats = Boat::for_user(db, &user).await; let coxes: Vec = futures::future::join_all( User::cox(db) @@ -78,7 +78,7 @@ async fn index( context.insert("logtypes", &logtypes); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.user, db).await, + &UserWithRoles::from_user(user.into(), db).await, ); context.insert("on_water", &on_water); context.insert("distances", &distances); @@ -87,12 +87,12 @@ async fn index( } #[get("/show", rank = 2)] -async fn show(db: &State, user: NonGuestUser) -> Template { +async fn show(db: &State, user: DonauLinzUser) -> Template { let logs = Logbook::completed(db).await; Template::render( "log.completed", - context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), + context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await), ) } @@ -166,12 +166,12 @@ async fn kiosk( async fn create_logbook( db: &SqlitePool, data: Form, - user: &NonGuestUser, + user: &DonauLinzUser, ) -> Flash { match Logbook::create( db, data.into_inner(), - &user.user + &user ) .await { @@ -197,14 +197,11 @@ async fn create_logbook( async fn create( db: &State, data: Form, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { Log::create( db, - format!( - "User {} tries to create log entry={:?}", - user.user.name, data - ), + format!("User {} tries to create log entry={:?}", &user.name, data), ) .await; @@ -238,14 +235,14 @@ async fn create_kiosk( ) .await; - create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme + create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme } async fn home_logbook( db: &SqlitePool, data: Form, logbook_id: i32, - user: &NonGuestUser, + user: &DonauLinzUser, ) -> Flash { let logbook: Option = Logbook::find_by_id(db, logbook_id).await; let Some(logbook) = logbook else { @@ -255,7 +252,7 @@ async fn home_logbook( ); }; - match logbook.home(db, &user.user, data.into_inner()).await { + match logbook.home(db,user, data.into_inner()).await { Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), @@ -285,11 +282,11 @@ async fn home_kiosk( db, data, logbook_id, - &NonGuestUser { - user: User::find_by_id(db, logbook.shipmaster as i32) + &DonauLinzUser( + User::find_by_id(db, logbook.shipmaster as i32) .await - .unwrap(), //TODO: fixme - }, + .unwrap(), + ), //TODO: fixme ) .await } @@ -299,13 +296,13 @@ async fn home( db: &State, data: Form, logbook_id: i32, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { Log::create( db, format!( "User {} tries to finish log entry {logbook_id} {data:?}", - user.user.name + &user.name ), ) .await; @@ -314,12 +311,12 @@ async fn home( } #[get("//delete", rank = 2)] -async fn delete(db: &State, logbook_id: i32, user: User) -> Flash { +async fn delete(db: &State, logbook_id: i32, user: DonauLinzUser) -> Flash { let logbook = Logbook::find_by_id(db, logbook_id).await; if let Some(logbook) = logbook { Log::create( db, - format!("User {} tries to delete log entry {logbook_id}", user.name), + format!("User {} tries to delete log entry {logbook_id}", &user.name), ) .await; match logbook.delete(db, &user).await { diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 7e3d6c0..17ab646 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -8,17 +8,12 @@ use rocket::{ response::{Flash, Redirect}, routes, Build, FromForm, Rocket, State, }; -use rocket_dyn_templates::{tera::Context, Template}; +use rocket_dyn_templates::Template; use serde::Deserialize; use sqlx::SqlitePool; +use tera::Context; -use crate::model::{ - log::Log, - tripdetails::TripDetails, - triptype::TripType, - user::{User, UserWithRoles}, - usertrip::{UserTrip, UserTripDeleteError, UserTripError}, -}; +use crate::model::user::{User, UserWithRoles}; pub(crate) mod admin; mod auth; @@ -27,6 +22,7 @@ mod cox; mod ergo; mod log; mod misc; +mod planned; mod stat; #[derive(FromForm, Debug)] @@ -35,6 +31,16 @@ struct LoginForm<'r> { password: &'r str, } +#[get("/")] +async fn index(db: &State, user: User, flash: Option>) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); + Template::render("index", context.into_json()) +} + #[post("/", data = "")] async fn wikiauth(db: &State, login: Form>) -> String { match User::login(db, login.name, login.password).await { @@ -43,164 +49,16 @@ async fn wikiauth(db: &State, login: Form>) -> String } } -#[get("/")] -async fn index(db: &State, user: User, flash: Option>) -> Template { - let mut context = Context::new(); - - if user.has_role(db, "cox").await || user.has_role(db, "admin").await { - let triptypes = TripType::all(db).await; - context.insert("trip_types", &triptypes); - } - - let days = user.get_days(db).await; - - if let Some(msg) = flash { - context.insert("flash", &msg.into_inner()); - } - context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); - context.insert("days", &days); - Template::render("index", context.into_json()) -} - -#[get("/join/?")] -async fn join( - db: &State, - trip_details_id: i64, - user: User, - user_note: Option, -) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "Trip_details do not exist."); - }; - - match UserTrip::create(db, &user, &trip_details, user_note).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} registered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!") - } - Err(UserTripError::EventAlreadyFull) => { - Flash::error(Redirect::to("/"), "Event bereits ausgebucht!") - } - Err(UserTripError::AlreadyRegistered) => { - Flash::error(Redirect::to("/"), "Du nimmst bereits teil!") - } - Err(UserTripError::AlreadyRegisteredAsCox) => { - Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!") - } - Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( - Redirect::to("/"), - "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", - ), - Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error( - Redirect::to("/"), - "Bei dieser Ausfahrt können leider keine Gäste mitfahren.", - ), - Err(UserTripError::NotAllowedToAddGuest) => Flash::error( - Redirect::to("/"), - "Du darfst keine Gäste hinzufügen.", - ), - Err(UserTripError::DetailsLocked) => Flash::error( - Redirect::to("/"), - "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.", - ), - } -} - -#[get("/remove//")] -async fn remove_guest( - db: &State, - trip_details_id: i64, - user: User, - name: String, -) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); - }; - - match UserTrip::delete(db, &user, &trip_details, Some(name)).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} unregistered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") - } - Err(UserTripDeleteError::DetailsLocked) => { - Log::create( - db, - format!( - "User {} tried to unregister for locked trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") - } - Err(UserTripDeleteError::GuestNotParticipating) => { - Flash::error(Redirect::to("/"), "Gast nicht angemeldet.") - } - Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( - Redirect::to("/"), - "Keine Berechtigung um den Gast zu entfernen.", - ), - } -} - -#[get("/remove/")] -async fn remove(db: &State, trip_details_id: i64, user: User) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); - }; - - match UserTrip::delete(db, &user, &trip_details, None).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} unregistered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") - } - Err(UserTripDeleteError::DetailsLocked) => { - Log::create( - db, - format!( - "User {} tried to unregister for locked trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") - } - Err(_) => { - panic!("Not possible to be here"); - } - } -} - -#[catch(401)] //unauthorized +#[catch(401)] //Unauthorized fn unauthorized_error() -> Redirect { Redirect::to("/auth") } +#[catch(403)] //forbidden +fn forbidden_error() -> Flash { + Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.") +} + #[derive(Deserialize)] #[serde(crate = "rocket::serde")] pub struct Config { @@ -210,10 +68,11 @@ pub struct Config { pub fn config(rocket: Rocket) -> Rocket { rocket - .mount("/", routes![index, join, remove, remove_guest]) + .mount("/", routes![index]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) .mount("/log", log::routes()) + .mount("/planned", planned::routes()) .mount("/ergo", ergo::routes()) .mount("/stat", stat::routes()) .mount("/boatdamage", boatdamage::routes()) @@ -221,7 +80,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/admin", admin::routes()) .mount("/", misc::routes()) .mount("/public", FileServer::from("static/")) - .register("/", catchers![unauthorized_error]) + .register("/", catchers![unauthorized_error, forbidden_error]) .attach(Template::fairing()) .attach(AdHoc::config::()) } @@ -255,7 +114,11 @@ mod test { assert_eq!(response.status(), Status::Ok); - assert!(response.into_string().await.unwrap().contains("Ausfahrten")); + assert!(response + .into_string() + .await + .unwrap() + .contains("Ruderassistent")); } #[sqlx::test] @@ -274,75 +137,6 @@ mod test { assert_eq!(response.headers().get("Location").next(), Some("/auth")); } - #[sqlx::test] - fn test_join_and_remove() { - 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=rower&password=rower"); // Add the form data to the request body; - login.dispatch().await; - - let req = client.get("/join/1"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); - - let req = client.get("/remove/1"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); - } - - #[sqlx::test] - fn test_join_invalid_event() { - 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=rower&password=rower"); // Add the form data to the request body; - login.dispatch().await; - - let req = client.get("/join/9999"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); - } - #[sqlx::test] fn test_public() { let db = testdb!(); diff --git a/src/tera/planned.rs b/src/tera/planned.rs new file mode 100644 index 0000000..0f9e00c --- /dev/null +++ b/src/tera/planned.rs @@ -0,0 +1,270 @@ +use rocket::{ + get, + request::FlashMessage, + response::{Flash, Redirect}, + routes, Route, State, +}; +use rocket_dyn_templates::Template; +use sqlx::SqlitePool; +use tera::Context; + +use crate::model::{ + log::Log, + tripdetails::TripDetails, + triptype::TripType, + user::{AllowedForPlannedTripsUser, User, UserWithRoles}, + usertrip::{UserTrip, UserTripDeleteError, UserTripError}, +}; + +#[get("/")] +async fn index( + db: &State, + user: AllowedForPlannedTripsUser, + flash: Option>, +) -> Template { + let user: User = user.into(); + + let mut context = Context::new(); + + if user.has_role(db, "cox").await || user.has_role(db, "admin").await { + let triptypes = TripType::all(db).await; + context.insert("trip_types", &triptypes); + } + + let days = user.get_days(db).await; + + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); + context.insert("days", &days); + Template::render("planned", context.into_json()) +} + +#[get("/join/?")] +async fn join( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, + user_note: Option, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/"), "Trip_details do not exist."); + }; + + match UserTrip::create(db, &user, &trip_details, user_note).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} registered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!") + } + Err(UserTripError::EventAlreadyFull) => { + Flash::error(Redirect::to("/"), "Event bereits ausgebucht!") + } + Err(UserTripError::AlreadyRegistered) => { + Flash::error(Redirect::to("/"), "Du nimmst bereits teil!") + } + Err(UserTripError::AlreadyRegisteredAsCox) => { + Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!") + } + Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( + Redirect::to("/"), + "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", + ), + Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error( + Redirect::to("/"), + "Bei dieser Ausfahrt können leider keine Gäste mitfahren.", + ), + Err(UserTripError::NotAllowedToAddGuest) => Flash::error( + Redirect::to("/"), + "Du darfst keine Gäste hinzufügen.", + ), + Err(UserTripError::DetailsLocked) => Flash::error( + Redirect::to("/"), + "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.", + ), + } +} + +#[get("/remove//")] +async fn remove_guest( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, + name: String, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); + }; + + match UserTrip::delete(db, &user, &trip_details, Some(name)).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} unregistered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") + } + Err(UserTripDeleteError::DetailsLocked) => { + Log::create( + db, + format!( + "User {} tried to unregister for locked trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") + } + Err(UserTripDeleteError::GuestNotParticipating) => { + Flash::error(Redirect::to("/"), "Gast nicht angemeldet.") + } + Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( + Redirect::to("/"), + "Keine Berechtigung um den Gast zu entfernen.", + ), + } +} + +#[get("/remove/")] +async fn remove( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); + }; + + match UserTrip::delete(db, &user, &trip_details, None).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} unregistered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") + } + Err(UserTripDeleteError::DetailsLocked) => { + Log::create( + db, + format!( + "User {} tried to unregister for locked trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") + } + Err(_) => { + panic!("Not possible to be here"); + } + } +} + +pub fn routes() -> Vec { + routes![index, join, remove, remove_guest] +} + +#[cfg(test)] +mod test { + use rocket::{ + http::{ContentType, Status}, + local::asynchronous::Client, + }; + use sqlx::SqlitePool; + + use crate::testdb; + + #[sqlx::test] + fn test_join_and_remove() { + 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=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/planned/join/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); + + let req = client.get("/planned/remove/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); + } + + #[sqlx::test] + fn test_join_invalid_event() { + 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=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/planned/join/9999"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); + } +} diff --git a/src/tera/stat.rs b/src/tera/stat.rs index 0dfee21..5db94fb 100644 --- a/src/tera/stat.rs +++ b/src/tera/stat.rs @@ -4,19 +4,19 @@ use sqlx::SqlitePool; use crate::model::{ stat::{self, Stat}, - user::{NonGuestUser, UserWithRoles}, + user::{DonauLinzUser, UserWithRoles}, }; use super::log::KioskCookie; #[get("/boats?", rank = 2)] -async fn index_boat(db: &State, user: NonGuestUser, year: Option) -> Template { +async fn index_boat(db: &State, user: DonauLinzUser, year: Option) -> Template { let stat = Stat::boats(db, year).await; let kiosk = false; Template::render( "stat.boats", - context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk), + context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk), ) } @@ -33,15 +33,15 @@ async fn index_boat_kiosk( } #[get("/?", rank = 2)] -async fn index(db: &State, user: NonGuestUser, year: Option) -> Template { +async fn index(db: &State, user: DonauLinzUser, year: Option) -> Template { let stat = Stat::people(db, year).await; let guest_km = Stat::guest(db, year).await; - let personal = stat::get_personal(db, &user.user).await; + let personal = stat::get_personal(db, &user).await; let kiosk = false; Template::render( "stat.people", - context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km), + context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km), ) } diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 3d4fbf3..c2691b4 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -10,7 +10,7 @@ -
+
+
{% if flash %} {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} {% endif %} - -

Ausfahrten

- - {% include "includes/buttons" %} - - {% for day in days %} - {% set amount_trips = day.planned_events | length + day.trips | length %} - {% set_global day_cox_needed = false %} - {% if day.planned_events | length > 0 %} - {% for planned_event in day.planned_events %} - {% if planned_event.cox_needed %} - {% set_global day_cox_needed = true %} - {% endif %} - {% endfor %} - {% endif %} - -
-
-

{{ day.day| date(format="%d.%m.%Y") }} - {{ day.day | date(format="%A", locale="de_AT") }} -

- - {% if day.planned_events | length > 0 or day.trips | length > 0 %} -
- - {# --- START Events --- #} - {% if day.planned_events | length > 0 %} - {% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %} - {% set amount_cur_cox = planned_event.cox | length %} - {% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %} -
-
- -
- {# --- START Row Buttons --- #} - {% set_global cur_user_participates = false %} - {% for rower in planned_event.rower%} - {% if rower.name == loggedin_user.name %} - {% set_global cur_user_participates = true %} - {% endif %} - {% endfor %} - {% if cur_user_participates %} - Abmelden - {% endif %} - {% if planned_event.max_people > planned_event.rower | length %} - {% if cur_user_participates == false %} - Mitrudern - {% endif %} - {% endif %} - {# --- END Row Buttons --- #} - - {# --- START Cox Buttons --- #} - {% if "cox" in loggedin_user.roles %} - {% set_global cur_user_participates = false %} - {% for cox in planned_event.cox %} - {% if cox.name == loggedin_user.name %} - {% set_global cur_user_participates = true %} - {% endif %} - {% endfor %} - {% if cur_user_participates %} - - {% include "includes/cox-icon" %} - Abmelden - - {% else %} - - {% include "includes/cox-icon" %} - Steuern - - {% endif %} - {% endif %} - {# --- END Cox Buttons --- #} -
-
- - {# --- START Sidebar Content --- #} - - {# --- END Sidebar Content --- #} -
- {% endfor %} - {% endif %} - {# --- END Events --- #} + {% if "scheckbuch" in loggedin_user.roles %} +
+ +
+ {% endif %} - {# --- START Trips --- #} - {% if day.trips | length > 0 %} - {% for trip in day.trips | sort(attribute="planned_starting_time") %} -
-
-
- {% if trip.max_people == 0 %} - ⚠ - {{ trip.planned_starting_time }} - Uhr - (Absage - {{ trip.cox_name }} - {% if trip.trip_type %} - - - {{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }} - {% endif %}) - {% else %} - {{ trip.planned_starting_time }} - Uhr - ({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %}) - {% endif %} -
- - Details - -
+ {% if "admin" in loggedin_user.roles %} +
+ +
+ {% endif %} -
- {% set_global cur_user_participates = false %} - {% for rower in trip.rower %} - {% if rower.name == loggedin_user.name %} - {% set_global cur_user_participates = true %} - {% endif %} - {% endfor %} - {% if cur_user_participates %} - Abmelden - {% endif %} - {% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} - Mitrudern - {% endif %} -
-
- {# --- START Sidebar Content --- #} - -
- {# --- END Sidebar Content --- #} -
- {% endfor %} - {% endif %} - {# --- END Trips --- #} -
- {% endif %} -
- - {# --- START Add Buttons --- #} - {% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %} -
- {% if "admin" in loggedin_user.roles %} - - - {% include "includes/plus-icon" %} - - Event - - {% endif %} - - {% if "cox" in loggedin_user.roles %} - - - {% include "includes/plus-icon" %} - - Ausfahrt - - {% endif %} -
- {% endif %} - {# --- END Add Buttons --- #} -
- {% endfor %} -
-
- -{% if "cox" in loggedin_user.roles %} - {% include "forms/trip" %} -{% endif %} - -{% if "admin" in loggedin_user.roles %} - {% include "forms/event" %} -{% endif %}{% endblock content %} +{% endblock content%} diff --git a/templates/planned.html.tera b/templates/planned.html.tera new file mode 100644 index 0000000..be6f862 --- /dev/null +++ b/templates/planned.html.tera @@ -0,0 +1,294 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} +
+ {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} + +

Ausfahrten

+ + {% include "includes/buttons" %} + + {% for day in days %} + {% set amount_trips = day.planned_events | length + day.trips | length %} + {% set_global day_cox_needed = false %} + {% if day.planned_events | length > 0 %} + {% for planned_event in day.planned_events %} + {% if planned_event.cox_needed %} + {% set_global day_cox_needed = true %} + {% endif %} + {% endfor %} + {% endif %} + +
+
+

{{ day.day| date(format="%d.%m.%Y") }} + {{ day.day | date(format="%A", locale="de_AT") }} +

+ + {% if day.planned_events | length > 0 or day.trips | length > 0 %} +
+ + {# --- START Events --- #} + {% if day.planned_events | length > 0 %} + {% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %} + {% set amount_cur_cox = planned_event.cox | length %} + {% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %} +
+
+
+ + {{ planned_event.planned_starting_time }} + Uhr + + ({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})
+ + + Details + +
+
+ {# --- START Row Buttons --- #} + {% set_global cur_user_participates = false %} + {% for rower in planned_event.rower%} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if planned_event.max_people > planned_event.rower | length %} + {% if cur_user_participates == false %} + Mitrudern + {% endif %} + {% endif %} + {# --- END Row Buttons --- #} + + {# --- START Cox Buttons --- #} + {% if "cox" in loggedin_user.roles %} + {% set_global cur_user_participates = false %} + {% for cox in planned_event.cox %} + {% if cox.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + + {% include "includes/cox-icon" %} + Abmelden + + {% else %} + + {% include "includes/cox-icon" %} + Steuern + + {% endif %} + {% endif %} + {# --- END Cox Buttons --- #} +
+
+ + {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Events --- #} + + {# --- START Trips --- #} + {% if day.trips | length > 0 %} + {% for trip in day.trips | sort(attribute="planned_starting_time") %} +
+
+
+ {% if trip.max_people == 0 %} + ⚠ + {{ trip.planned_starting_time }} + Uhr + (Absage + {{ trip.cox_name }} + {% if trip.trip_type %} + - + {{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }} + {% endif %}) + {% else %} + {{ trip.planned_starting_time }} + Uhr + ({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %}) + {% endif %} +
+ + Details + +
+ +
+ {% set_global cur_user_participates = false %} + {% for rower in trip.rower %} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} + Mitrudern + {% endif %} +
+
+ {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Trips --- #} +
+ {% endif %} +
+ + {# --- START Add Buttons --- #} + {% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %} +
+ {% if "admin" in loggedin_user.roles %} + + + {% include "includes/plus-icon" %} + + Event + + {% endif %} + + {% if "cox" in loggedin_user.roles %} + + + {% include "includes/plus-icon" %} + + Ausfahrt + + {% endif %} +
+ {% endif %} + {# --- END Add Buttons --- #} +
+ {% endfor %} +
+ + +{% if "cox" in loggedin_user.roles %} + {% include "forms/trip" %} +{% endif %} + +{% if "admin" in loggedin_user.roles %} + {% include "forms/event" %} +{% endif %}{% endblock content %}