rowt/src/tera/mod.rs
philipp d9e8f6170c
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 11m7s
Merge branch 'main' into update-ergo
2024-10-25 20:15:11 +02:00

359 lines
10 KiB
Rust

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<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> 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<SqlitePool>, user: Option<User>) -> 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<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> 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 = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> 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 = "<blogpost>")]
async fn new_blogpost(
db: &State<SqlitePool>,
blogpost: Form<NewBlogpostForm<'_>>,
config: &State<Config>,
) -> 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 = "<blogpost>")]
async fn blogpost_unpublished(
db: &State<SqlitePool>,
blogpost: Form<BlogpostUnpublishedForm<'_>>,
config: &State<Config>,
) -> 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<Redirect> {
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::<i32>() {
Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().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::<Config>().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<Build>) -> Rocket<Build> {
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::<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);
}
}