use std::{fs::OpenOptions, io::Write}; use chrono::{Datelike, 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, personal::Achievements, 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; pub(crate) mod trailerreservation; #[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); } let date = chrono::Utc::now(); if date.month() <= 3 || date.month() >= 10 { context.insert("show_quick_ergo_button", "yes"); } context.insert("achievements", &Achievements::for_user(db, &user).await); 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| 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 { if let Ok(user) = User::login(db, login.name, login.password).await { if user.has_role(db, "allow_website_login").await { return String::from("SUCC"); } if user.has_role(db, "admin").await { return String::from("SUCC"); } if user.has_role(db, "Vorstand").await { return String::from("SUCC"); } } "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") } #[derive(FromForm, Debug)] struct NewBlogpostForm<'r> { article_url: &'r str, article_title: &'r str, pw: &'r str, } #[post("/", data = "")] async fn new_blogpost( db: &State, blogpost: Form>, config: &State, ) -> String { if blogpost.pw == config.wordpress_key { let member = Role::find_by_name(db, "Donau Linz").await.unwrap(); Notification::create_for_role( db, &member, &urlencoding::decode(blogpost.article_title).expect("UTF-8"), "Neuer Blogpost", Some(blogpost.article_url), None, ) .await; "ACK".into() } else { "WRONG pw".into() } } #[derive(FromForm, Debug)] struct BlogpostUnpublishedForm<'r> { article_url: &'r str, pw: &'r str, } #[post("/", data = "")] async fn blogpost_unpublished( db: &State, blogpost: Form>, config: &State, ) -> String { if blogpost.pw == config.wordpress_key { Notification::delete_by_link( db, &urlencoding::decode(blogpost.article_url).expect("UTF-8"), ) .await; "ACK".into() } else { "WRONG pw".into() } } #[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, wordpress_key: String, } pub fn config(rocket: Rocket) -> Rocket { rocket .mount("/", routes![index, steering, impressum]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) .mount("/new-blogpost", routes![new_blogpost]) .mount("/blogpost-unpublished", routes![blogpost_unpublished]) .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("/trailerreservation", trailerreservation::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); } }