284 lines
8.4 KiB
Rust
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);
|
|
}
|
|
}
|