use std::{fs::OpenOptions, io::Write}; use chrono::Local; use rocket::{ catch, catchers, fairing::{AdHoc, Fairing, Info, Kind}, form::Form, fs::FileServer, get, http::Cookie, post, request::FlashMessage, response::{Flash, Redirect}, routes, time::{Duration, OffsetDateTime}, Build, Data, FromForm, Request, Rocket, State, }; use rocket_dyn_templates::Template; use serde::Deserialize; use sqlx::SqlitePool; use tera::Context; use crate::model::{ logbook::Logbook, notification::Notification, role::Role, user::{User, UserWithDetails, SCHECKBUCH}, }; pub(crate) mod admin; mod auth; pub(crate) mod board; mod boatdamage; pub(crate) mod boatreservation; mod cox; mod ergo; mod log; mod misc; mod notification; mod planned; mod stat; #[derive(FromForm, Debug)] struct LoginForm<'r> { name: &'r str, 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()); } if user.has_role(db, "scheckbuch").await { let last_trips = Logbook::completed_with_user(db, &user).await; context.insert("last_trips", &last_trips); } context.insert("notifications", &Notification::for_user(db, &user).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("costs_scheckbuch", &SCHECKBUCH); Template::render("index", context.into_json()) } #[get("/impressum")] async fn impressum(db: &State, user: Option) -> Template { let mut context = Context::new(); if let Some(user) = user { context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); } Template::render("impressum", context.into_json()) } #[get("/steering")] async fn steering(db: &State, user: User, flash: Option>) -> Template { let mut context = Context::new(); if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } let bootskundige = User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await; let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await; coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list coxes.retain(|user| user.name != "Externe Steuerperson"); context.insert("coxes", &coxes); context.insert("bootskundige", &bootskundige); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); Template::render("steering", context.into_json()) } #[post("/", data = "")] async fn wikiauth(db: &State, login: Form>) -> String { match User::login(db, login.name, login.password).await { Ok(_) => "SUCC".into(), Err(_) => "FAIL".into(), } } #[catch(401)] //Unauthorized fn unauthorized_error(req: &Request) -> Redirect { // Save the URL the user tried to access, to be able to go there once logged in let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri())); println!("{}", req.uri()); redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1)); req.cookies().add_private(redirect_cookie); 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.") } struct Usage {} #[rocket::async_trait] impl Fairing for Usage { fn info(&self) -> Info { Info { name: "Usage stats of website", kind: Kind::Request, } } // Increment the counter for `GET` and `POST` requests. async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) { let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S"); let user = match req.cookies().get_private("loggedin_user") { Some(user_id) => match user_id.value().parse::() { Ok(user_id) => { let db = req.rocket().state::().unwrap(); if let Some(user) = User::find_by_id(db, user_id).await { format!("User: {}", user.name) } else { format!("USER ID {user_id} NOT EXISTS") } } Err(_) => format!("INVALID USER ID ({user_id})"), }, None => "NOT LOGGED IN".to_string(), }; let uri = req.uri().to_string(); if !uri.ends_with(".css") && !uri.ends_with(".js") && !uri.ends_with(".ico") && !uri.ends_with(".json") && !uri.ends_with(".png") { let config = req.rocket().state::().unwrap(); let Ok(mut file) = OpenOptions::new() .append(true) .open(config.usage_log_path.clone()) else { eprintln!( "File {} can't be found, not saving usage logs", config.usage_log_path.clone() ); return; }; if let Err(e) = writeln!(file, "{timestamp};{user};{uri}") { eprintln!("Couldn't write to file: {}", e); } } } } #[derive(Deserialize)] #[serde(crate = "rocket::serde")] pub struct Config { rss_key: String, smtp_pw: String, usage_log_path: String, pub openweathermap_key: String, } pub fn config(rocket: Rocket) -> Rocket { rocket .mount("/", routes![index, steering, impressum]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) .mount("/log", log::routes()) .mount("/planned", planned::routes()) .mount("/ergo", ergo::routes()) .mount("/notification", notification::routes()) .mount("/stat", stat::routes()) .mount("/boatdamage", boatdamage::routes()) .mount("/boatreservation", boatreservation::routes()) .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) .mount("/board", board::routes()) .mount("/", misc::routes()) .mount("/public", FileServer::from("static/")) .register("/", catchers![unauthorized_error, forbidden_error]) .attach(Template::fairing()) .attach(AdHoc::config::()) .attach(Usage {}) } #[cfg(test)] mod test { use rocket::{ http::{ContentType, Status}, local::asynchronous::Client, }; use sqlx::SqlitePool; use crate::testdb; #[sqlx::test] fn test_index() { 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=cox&password=cox"); // Add the form data to the request body; login.dispatch().await; let req = client.get("/"); let response = req.dispatch().await; assert_eq!(response.status(), Status::Ok); assert!(response .into_string() .await .unwrap() .contains("Ruderassistent")); } #[sqlx::test] fn test_without_login() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/"); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/auth")); } #[sqlx::test] fn test_public() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/public/main.css"); let response = req.dispatch().await; assert_eq!(response.status(), Status::Ok); let req = client.get("/public/main.js"); let response = req.dispatch().await; assert_eq!(response.status(), Status::Ok); } }