rowt/src/tera/mod.rs
philipp 4bd91b2a7e
Some checks failed
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) Has been cancelled
add impressum
2024-05-17 12:26:10 +02:00

284 lines
8.4 KiB
Rust

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<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);
}
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| !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 = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> 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<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,
}
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
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::<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);
}
}