From 588520914cc705f8596149681ce6fc17d5d883e6 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Wed, 16 Apr 2025 10:18:27 +0200 Subject: [PATCH 01/20] add nextcloud auth route --- src/tera/mod.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 486661e..1396a76 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -7,7 +7,7 @@ use rocket::{ form::Form, fs::FileServer, get, - http::Cookie, + http::{Cookie, Status}, post, request::FlashMessage, response::{Flash, Redirect}, @@ -123,11 +123,23 @@ async fn wikiauth(db: &State, login: Form>) -> String "FAIL".into() } +#[get("/?&")] +async fn nextcloud_auth(db: &State, username: String, password: String) -> Status { + if let Ok(user) = User::login(db, &username, &password).await { + if user.has_role(db, "admin").await { + return Status::Ok; + } + if user.has_role(db, "Vorstand").await { + return Status::Ok; + } + } + Status::Unauthorized +} + #[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); @@ -265,6 +277,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/", routes![index, steering, impressum]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) + .mount("/nxauth", routes![nextcloud_auth]) .mount("/new-blogpost", routes![new_blogpost]) .mount("/blogpost-unpublished", routes![blogpost_unpublished]) .mount("/log", log::routes()) From 2b79df8e4290c6d7f2800c7086cf815aae54bf83 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Wed, 16 Apr 2025 10:46:19 +0200 Subject: [PATCH 02/20] no funny business w/ get params --- Cargo.lock | 1 + Cargo.toml | 1 + src/tera/mod.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3f3426..ea63be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2544,6 +2544,7 @@ name = "rot" version = "0.1.0" dependencies = [ "argon2", + "base64", "chrono", "chrono-tz 0.10.3", "csv", diff --git a/Cargo.toml b/Cargo.toml index 4a16639..921b158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ job_scheduler_ng = "2.0" ureq = { version = "3.0", features = ["json"] } regex = "1.10" urlencoding = "2.1" +base64 = "0.22" [target.'cfg(not(windows))'.dependencies] openssl = { version = "0.10", features = [ "vendored" ] } diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 1396a76..44d0890 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -9,7 +9,7 @@ use rocket::{ get, http::{Cookie, Status}, post, - request::FlashMessage, + request::{FlashMessage, FromRequest, Outcome}, response::{Flash, Redirect}, routes, time::{Duration, OffsetDateTime}, @@ -123,9 +123,57 @@ async fn wikiauth(db: &State, login: Form>) -> String "FAIL".into() } -#[get("/?&")] -async fn nextcloud_auth(db: &State, username: String, password: String) -> Status { - if let Ok(user) = User::login(db, &username, &password).await { +struct BasicAuth { + username: String, + password: String, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for BasicAuth { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + // Get the Authorization header + let auth_header = match request.headers().get_one("Authorization") { + Some(h) => h, + None => return Outcome::Failure((Status::Unauthorized, ())), + }; + + // Check if it's a Basic auth header + if !auth_header.starts_with("Basic ") { + return Outcome::Failure((Status::Unauthorized, ())); + } + + // Decode the base64 credentials + let credentials = match BASE64.decode(auth_header[6..].as_bytes()) { + Ok(c) => c, + Err(_) => return Outcome::Failure((Status::Unauthorized, ())), + }; + + // Convert to UTF-8 string + let credentials_str = match str::from_utf8(&credentials) { + Ok(s) => s, + Err(_) => return Outcome::Failure((Status::Unauthorized, ())), + }; + + // Split into username and password + let mut parts = credentials_str.splitn(2, ':'); + let username = match parts.next() { + Some(u) => u.to_string(), + None => return Outcome::Failure((Status::Unauthorized, ())), + }; + let password = match parts.next() { + Some(p) => p.to_string(), + None => return Outcome::Failure((Status::Unauthorized, ())), + }; + + Outcome::Success(BasicAuth { username, password }) + } +} + +#[get("/")] +async fn nextcloud_auth(db: &State, auth: BasicAuth) -> Status { + if let Ok(user) = User::login(db, &auth.username, &auth.password).await { if user.has_role(db, "admin").await { return Status::Ok; } From dc2ee38aa024816b1f9892d1cab333e0e496ae14 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Wed, 16 Apr 2025 10:56:57 +0200 Subject: [PATCH 03/20] no funny business w/ get params --- src/tera/mod.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 44d0890..44cba1a 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -30,6 +30,7 @@ use crate::{ }, SCHECKBUCH, }; +use base64::alphabet::STANDARD; pub(crate) mod admin; mod auth; @@ -136,35 +137,35 @@ impl<'r> FromRequest<'r> for BasicAuth { // Get the Authorization header let auth_header = match request.headers().get_one("Authorization") { Some(h) => h, - None => return Outcome::Failure((Status::Unauthorized, ())), + None => return Outcome::Error((Status::Unauthorized, ())), }; // Check if it's a Basic auth header if !auth_header.starts_with("Basic ") { - return Outcome::Failure((Status::Unauthorized, ())); + return Outcome::Error((Status::Unauthorized, ())); } // Decode the base64 credentials - let credentials = match BASE64.decode(auth_header[6..].as_bytes()) { + let credentials = match base64::decode(&auth_header[6..]) { Ok(c) => c, - Err(_) => return Outcome::Failure((Status::Unauthorized, ())), + Err(_) => return Outcome::Error((Status::Unauthorized, ())), }; // Convert to UTF-8 string - let credentials_str = match str::from_utf8(&credentials) { + let credentials_str = match std::str::from_utf8(&credentials) { Ok(s) => s, - Err(_) => return Outcome::Failure((Status::Unauthorized, ())), + Err(_) => return Outcome::Error((Status::Unauthorized, ())), }; // Split into username and password let mut parts = credentials_str.splitn(2, ':'); let username = match parts.next() { Some(u) => u.to_string(), - None => return Outcome::Failure((Status::Unauthorized, ())), + None => return Outcome::Error((Status::Unauthorized, ())), }; let password = match parts.next() { Some(p) => p.to_string(), - None => return Outcome::Failure((Status::Unauthorized, ())), + None => return Outcome::Error((Status::Unauthorized, ())), }; Outcome::Success(BasicAuth { username, password }) From 4ce9a573fe9087f6a29ef25664360182168e056e Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Wed, 16 Apr 2025 11:31:46 +0200 Subject: [PATCH 04/20] 400 instead of 303 --- src/tera/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 44cba1a..3e403fa 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -182,7 +182,7 @@ async fn nextcloud_auth(db: &State, auth: BasicAuth) -> Status { return Status::Ok; } } - Status::Unauthorized + Status::BadRequest } #[catch(401)] //Unauthorized From cf90ab6e1ade17e5cf6a0bad521b3ef84d390a54 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 17 Apr 2025 20:38:41 +0200 Subject: [PATCH 05/20] allow others to send fee reminder thus reducing my bus factor --- Rocket.toml | 2 +- src/model/mail.rs | 29 +++++++++++++------- src/model/user/mod.rs | 1 + src/tera/admin/mail.rs | 50 +++++++++++++++++++++++++++------- src/tera/mod.rs | 1 - templates/admin/mail.html.tera | 26 ++++++++++++++++++ 6 files changed, 87 insertions(+), 22 deletions(-) diff --git a/Rocket.toml b/Rocket.toml index cc93d2b..4b4856e 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -2,7 +2,7 @@ secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" rss_key = "rss-key-for-ci" limits = { file = "10 MiB", data-form = "10 MiB"} -smtp_pw = "8kIjlLH79Ky6D3j" +smtp_pw = "8kIjlLH79Ky6D3jQ" usage_log_path = "./usage.txt" openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5" wordpress_key = "pw-to-allow-sending-notifications" diff --git a/src/model/mail.rs b/src/model/mail.rs index 3d3794b..9879630 100644 --- a/src/model/mail.rs +++ b/src/model/mail.rs @@ -151,10 +151,15 @@ impl Mail { false } - pub async fn fees(db: &SqlitePool, smtp_pw: String) { + pub async fn fees(db: &SqlitePool, smtp_pw: String, test: Option) { let users = User::all_payer_groups(db).await; for user in users { - if !user.has_role(db, "paid").await { + if let Some(test) = &test { + if user.id != test.id { + continue; + } + } + if !user.has_role(db, "paid").await || test.is_some() { let mut is_family = false; let mut send_to = String::new(); match Family::find_by_opt_id(db, user.family_id).await { @@ -196,11 +201,10 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€", )) } content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\ -Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\ +Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\ Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n Beste Grüße\n\ -Der Vorstand - "); +Der Vorstand"); let mut email = Message::builder() .from( "ASKÖ Ruderverein Donau Linz " @@ -208,7 +212,7 @@ Der Vorstand .unwrap(), ) .reply_to( - "ASKÖ Ruderverein Donau Linz " + "ASKÖ Ruderverein Donau Linz " .parse() .unwrap(), ) @@ -253,11 +257,16 @@ Der Vorstand } } - pub async fn fees_final(db: &SqlitePool, smtp_pw: String) { + pub async fn fees_final(db: &SqlitePool, smtp_pw: String, test: Option) { let users = User::all_payer_groups(db).await; for user in users { + if let Some(test) = &test { + if user.id != test.id { + continue; + } + } if let Some(fee) = user.fee(db).await { - if !fee.paid { + if !fee.paid || test.is_some() { let mut is_family = false; let mut send_to = String::new(); match Family::find_by_opt_id(db, user.family_id).await { @@ -282,7 +291,7 @@ Der Vorstand "Liebes Vereinsmitglied, \n\n\ wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können. -Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\ +Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch binnen 14 Tagen, auf unser Bankkonto.\n\n\ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€", fees.sum_in_cents / 100, ); @@ -317,7 +326,7 @@ Der Vorstand"); .unwrap(), ) .reply_to( - "ASKÖ Ruderverein Donau Linz " + "ASKÖ Ruderverein Donau Linz " .parse() .unwrap(), ) diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 38862e3..b37879d 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -1182,6 +1182,7 @@ special_user!(VorstandUser, +"admin", +"Vorstand"); special_user!(EventUser, +"manage_events"); special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin"); special_user!(ManageUserUser, +"admin", +"schriftfuehrer"); +special_user!(AllowedToSendFeeReminderUser, +"admin", +"schriftfuehrer", +"kassier"); special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin"); #[derive(FromRow, Serialize, Deserialize, Clone, Debug)] diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs index dd9c8d3..953bf23 100644 --- a/src/tera/admin/mail.rs +++ b/src/tera/admin/mail.rs @@ -9,8 +9,8 @@ use sqlx::SqlitePool; use crate::model::log::Log; use crate::model::mail::Mail; use crate::model::role::Role; -use crate::model::user::UserWithDetails; -use crate::model::user::{AdminUser, VorstandUser}; +use crate::model::user::VorstandUser; +use crate::model::user::{AllowedToSendFeeReminderUser, UserWithDetails}; use crate::tera::Config; #[get("/mail")] @@ -35,21 +35,51 @@ async fn index( } #[get("/mail/fee")] -async fn fee(db: &State, admin: AdminUser, config: &State) -> &'static str { +async fn fee( + db: &State, + admin: AllowedToSendFeeReminderUser, + config: &State, +) -> Flash { Log::create(db, format!("{admin:?} trying to send fee")).await; - Mail::fees(db, config.smtp_pw.clone()).await; - "SUCC" + Mail::fees(db, config.smtp_pw.clone(), None).await; + Log::create(db, "Mail successfully sent".into()).await; + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") +} + +#[get("/mail/fee/test")] +async fn fee_test( + db: &State, + admin: AllowedToSendFeeReminderUser, + config: &State, +) -> Flash { + Log::create(db, format!("{admin:?} trying to send test fee")).await; + Mail::fees(db, config.smtp_pw.clone(), Some(admin.user)).await; + Log::create(db, "Mail successfully sent".into()).await; + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") } #[get("/mail/fee-final")] async fn fee_final( db: &State, - admin: AdminUser, + admin: AllowedToSendFeeReminderUser, config: &State, -) -> &'static str { +) -> Flash { Log::create(db, format!("{admin:?} trying to send fee_final")).await; - Mail::fees_final(db, config.smtp_pw.clone()).await; - "SUCC" + Mail::fees_final(db, config.smtp_pw.clone(), None).await; + Log::create(db, "Mail successfully sent".into()).await; + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") +} + +#[get("/mail/fee-final/test")] +async fn fee_final_test( + db: &State, + admin: AllowedToSendFeeReminderUser, + config: &State, +) -> Flash { + Log::create(db, format!("{admin:?} trying to send test fee_final")).await; + Mail::fees_final(db, config.smtp_pw.clone(), Some(admin.user)).await; + Log::create(db, "Mail successfully sent".into()).await; + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") } #[derive(FromForm, Debug)] @@ -79,7 +109,7 @@ async fn update( } pub fn routes() -> Vec { - routes![index, update, fee, fee_final] + routes![index, update, fee, fee_test, fee_final, fee_final_test] } #[cfg(test)] diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 486661e..ac3a130 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -127,7 +127,6 @@ async fn wikiauth(db: &State, login: Form>) -> String 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); diff --git a/templates/admin/mail.html.tera b/templates/admin/mail.html.tera index 6c97d4c..d17e998 100644 --- a/templates/admin/mail.html.tera +++ b/templates/admin/mail.html.tera @@ -22,6 +22,32 @@ + + {% endblock content %} From db429b6fe31b263ab62b4b5c818171721a99920c Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 17 Apr 2025 20:42:36 +0200 Subject: [PATCH 06/20] high security application... --- Rocket.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rocket.toml b/Rocket.toml index 4b4856e..88a9b6b 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -2,7 +2,7 @@ secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" rss_key = "rss-key-for-ci" limits = { file = "10 MiB", data-form = "10 MiB"} -smtp_pw = "8kIjlLH79Ky6D3jQ" +smtp_pw = "my-smtp-password" usage_log_path = "./usage.txt" -openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5" +openweathermap_key = "openweather-key" wordpress_key = "pw-to-allow-sending-notifications" From b0a2d3d5393705fdc9fb15fab8acd5fc865e331c Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 17 Apr 2025 21:52:24 +0200 Subject: [PATCH 07/20] better description of the button --- templates/admin/mail.html.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/mail.html.tera b/templates/admin/mail.html.tera index d17e998..c80db2e 100644 --- a/templates/admin/mail.html.tera +++ b/templates/admin/mail.html.tera @@ -44,7 +44,7 @@ - An ALLE Mitglieder versenden + An ALLE Mitglieder versenden, die noch nicht bezahlt haben From 36245fd0f7404c17708476f942a716b8531bbc75 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 17 Apr 2025 22:00:15 +0200 Subject: [PATCH 08/20] use maries' magic css skills to unbreak signal links on mobile; Fixes #891 --- templates/index.html.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html.tera b/templates/index.html.tera index 0edb986..ea8b689 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -333,7 +333,7 @@ Signal-Gruppenchat Steuerpersonen Donau Linz

- Mit diesem Link kannst du unserer Signal Gruppe beitreten: https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp + Mit diesem Link kannst du unserer Signal Gruppe beitreten: https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp

From 0059dfe96ff6227bbf29acfa71bab90efe3887f0 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 18 Apr 2025 17:04:10 +0200 Subject: [PATCH 09/20] simple nx auth --- src/tera/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tera/mod.rs b/src/tera/mod.rs index ac3a130..91f6ecb 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -123,6 +123,19 @@ async fn wikiauth(db: &State, login: Form>) -> String "FAIL".into() } +#[post("/", data = "")] +async fn nextcloud_auth(db: &State, login: Form>) -> String { + if let Ok(user) = User::login(db, login.name, login.password).await { + 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 @@ -264,6 +277,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/", routes![index, steering, impressum]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) + .mount("/nxauth", routes![nextcloud_auth]) .mount("/new-blogpost", routes![new_blogpost]) .mount("/blogpost-unpublished", routes![blogpost_unpublished]) .mount("/log", log::routes()) From 37b6ea60574c928d70ee50026fabd2805fb4e3f6 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 18 Apr 2025 17:44:21 +0200 Subject: [PATCH 10/20] remove unused dep; cargo clippy --- Cargo.lock | 1 - Cargo.toml | 1 - src/model/tripdetails.rs | 2 +- src/tera/mod.rs | 5 ++--- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea63be2..b3f3426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2544,7 +2544,6 @@ name = "rot" version = "0.1.0" dependencies = [ "argon2", - "base64", "chrono", "chrono-tz 0.10.3", "csv", diff --git a/Cargo.toml b/Cargo.toml index 921b158..4a16639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ job_scheduler_ng = "2.0" ureq = { version = "3.0", features = ["json"] } regex = "1.10" urlencoding = "2.1" -base64 = "0.22" [target.'cfg(not(windows))'.dependencies] openssl = { version = "0.10", features = [ "vendored" ] } diff --git a/src/model/tripdetails.rs b/src/model/tripdetails.rs index ff813cb..eebafce 100644 --- a/src/model/tripdetails.rs +++ b/src/model/tripdetails.rs @@ -196,7 +196,7 @@ WHERE day = ? AND planned_starting_time = ? .fetch_one(db) .await .unwrap(); //TODO: fixme - let amount_currently_registered = i64::from(amount_currently_registered.count); + let amount_currently_registered = amount_currently_registered.count; amount_currently_registered >= self.max_people } diff --git a/src/tera/mod.rs b/src/tera/mod.rs index b9e8dcb..91f6ecb 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -7,9 +7,9 @@ use rocket::{ form::Form, fs::FileServer, get, - http::{Cookie, Status}, + http::Cookie, post, - request::{FlashMessage, FromRequest, Outcome}, + request::FlashMessage, response::{Flash, Redirect}, routes, time::{Duration, OffsetDateTime}, @@ -30,7 +30,6 @@ use crate::{ }, SCHECKBUCH, }; -use base64::alphabet::STANDARD; pub(crate) mod admin; mod auth; From 10740f988dd145f78d6f58932d803df3a1db29bc Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 18 Apr 2025 23:01:17 +0200 Subject: [PATCH 11/20] reduce amount of magic values, goal is to have specific states -> e.g. cancelled --- src/model/event.rs | 32 +++++++++++++++++++++----------- src/model/mod.rs | 4 ++-- src/model/notification.rs | 2 +- src/model/trip.rs | 18 ++++++++++++------ src/model/tripdetails.rs | 2 +- templates/planned.html.tera | 15 +++++++-------- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/model/event.rs b/src/model/event.rs index fe63998..90eb9c0 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -34,11 +34,13 @@ pub struct Event { } #[derive(Serialize, Debug)] -pub struct EventWithUserAndTriptype { +pub struct EventWithDetails { #[serde(flatten)] pub event: Event, trip_type: Option, + tripdetails: TripDetails, cox_needed: bool, + cancelled: bool, cox: Vec, rower: Vec, } @@ -116,6 +118,12 @@ pub struct EventUpdate<'a> { pub trip_type_id: Option, } +impl EventUpdate<'_> { + fn cancelled(&self) -> bool { + self.max_people == -1 + } +} + impl Event { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( @@ -134,16 +142,13 @@ WHERE planned_event.id like ? .ok() } - pub async fn get_pinned_for_day( - db: &SqlitePool, - day: NaiveDate, - ) -> Vec { + pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { let mut events = Self::get_for_day(db, day).await; events.retain(|e| e.event.always_show); events } - pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { + pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { let day = format!("{day}"); let events = sqlx::query_as!( Event, @@ -164,10 +169,15 @@ WHERE day=?", if let Some(trip_type_id) = event.trip_type_id { trip_type = TripType::find_by_id(db, trip_type_id).await; } - ret.push(EventWithUserAndTriptype { + let tripdetails = TripDetails::find_by_id(db, event.trip_details_id) + .await + .expect("db constraints"); + ret.push(EventWithDetails { cox_needed: event.planned_amount_cox > cox.len() as i64, cox, rower: Registration::all_rower(db, event.trip_details_id).await, + cancelled: tripdetails.cancelled(), + tripdetails, event, trip_type, }); @@ -315,7 +325,7 @@ WHERE trip_details.id=? .unwrap(); //Okay, as planned_event can only be created with proper DB backing let tripdetails = self.trip_details(db).await; - let was_already_cancelled = tripdetails.max_people == 0; + let was_already_cancelled = tripdetails.cancelled(); sqlx::query!( "UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?", @@ -340,7 +350,7 @@ WHERE trip_details.id=? .await; } - if update.max_people == 0 && !was_already_cancelled { + if update.cancelled() && !was_already_cancelled { let coxes = Registration::all_cox(db, self.id).await; for user in coxes { if let Some(user) = User::find_by_name(db, &user.name).await { @@ -389,7 +399,7 @@ WHERE trip_details.id=? } } } - if update.max_people > 0 && was_already_cancelled { + if !update.cancelled() && was_already_cancelled { Notification::delete_by_action( db, &format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id), @@ -427,7 +437,7 @@ WHERE trip_details.id=? } pub fn is_cancelled(&self) -> bool { - self.max_people == 0 + self.max_people == -1 } pub async fn get_ics_feed(db: &SqlitePool) -> String { diff --git a/src/model/mod.rs b/src/model/mod.rs index 3a76436..b9d45d3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -6,7 +6,7 @@ use waterlevel::WaterlevelDay; use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use self::{ - event::{Event, EventWithUserAndTriptype}, + event::{Event, EventWithDetails}, trip::{Trip, TripWithUserAndType}, waterlevel::Waterlevel, weather::Weather, @@ -44,7 +44,7 @@ pub mod weather; #[derive(Serialize, Debug)] pub struct Day { day: NaiveDate, - events: Vec, + events: Vec, trips: Vec, is_pinned: bool, regular_sees_this_day: bool, diff --git a/src/model/notification.rs b/src/model/notification.rs index 8a49f37..1c74a1a 100644 --- a/src/model/notification.rs +++ b/src/model/notification.rs @@ -278,7 +278,7 @@ mod test { let cancel_update = EventUpdate { name: &event.name, planned_amount_cox: event.planned_amount_cox as i32, - max_people: 0, + max_people: -1, notes: event.notes.as_deref(), always_show: event.always_show, is_locked: event.is_locked, diff --git a/src/model/trip.rs b/src/model/trip.rs index eb5f82f..7565ea0 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -46,6 +46,12 @@ pub struct TripUpdate<'a> { pub is_locked: bool, } +impl<'a> TripUpdate<'a> { + fn cancelled(&self) -> bool { + self.max_people == -1 + } +} + impl TripWithUserAndType { pub async fn from(db: &SqlitePool, trip: Trip) -> Self { let mut trip_type = None; @@ -245,7 +251,7 @@ WHERE trip.id=? return Err(CoxHelpError::DetailsLocked); } - if event.max_people == 0 { + if event.is_cancelled() { return Err(CoxHelpError::CanceledEvent); } @@ -309,9 +315,9 @@ WHERE day=? }; let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); - let was_already_cancelled = tripdetails.max_people == 0; + let was_already_cancelled = tripdetails.cancelled(); - let is_locked = if update.max_people == 0 { + let is_locked = if update.cancelled() { false } else { update.is_locked @@ -329,7 +335,7 @@ WHERE day=? .await .unwrap(); //Okay, as trip_details can only be created with proper DB backing - if update.max_people == 0 && !was_already_cancelled { + if update.cancelled() && !was_already_cancelled { let rowers = TripWithUserAndType::from(db, update.trip.clone()) .await .rower; @@ -368,7 +374,7 @@ WHERE day=? .await; } - if update.max_people > 0 && was_already_cancelled { + if !update.cancelled() && was_already_cancelled { Notification::delete_by_action( db, &format!("remove_user_trip_with_trip_details_id:{}", trip_details_id), @@ -463,7 +469,7 @@ WHERE day=? } fn is_cancelled(&self) -> bool { - self.max_people == 0 + self.max_people == -1 } } diff --git a/src/model/tripdetails.rs b/src/model/tripdetails.rs index eebafce..46b74f5 100644 --- a/src/model/tripdetails.rs +++ b/src/model/tripdetails.rs @@ -95,7 +95,7 @@ WHERE day = ? AND planned_starting_time = ? } pub fn cancelled(&self) -> bool { - self.max_people == 0 + self.max_people == -1 } /// This function is called when a person registers to a trip or when the cox changes the diff --git a/templates/planned.html.tera b/templates/planned.html.tera index 4310925..e315430 100644 --- a/templates/planned.html.tera +++ b/templates/planned.html.tera @@ -124,7 +124,7 @@ {% if event.always_show and not day.regular_sees_this_day %} 🔮 {% endif -%} - {%- if event.max_people == 0 %} + {%- if event.cancelled %} ⚠ Absage {{ event.planned_starting_time }} Uhr @@ -202,7 +202,7 @@
{# --- START List Coxes --- #} {% if event.planned_amount_cox > 0 %} - {% if event.max_people == 0 %} + {% if event.cancelled %} {{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }} {% else %} {% if amount_cox_missing > 0 %} @@ -215,7 +215,7 @@ {# --- END List Coxes --- #} {# --- START List Rowers --- #} {% set amount_cur_rower = event.rower | length %} - {% if event.max_people == 0 %} + {% if event.cancelled %} {{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }} {% else %} {{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }} @@ -240,7 +240,7 @@ {{ macros::input(label='Titel', name='name', type='input', value=event.name) }} - {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }} + {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='0') }} {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }} {{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }} {{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }} @@ -260,7 +260,7 @@
{% else %} - {% if event.max_people == 0 %} + {% if event.cancelled %} Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen. {% else %}
@@ -269,9 +269,8 @@ {{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }} - {{ macros::input(label='', name='max_people', type='hidden', value=0) }} + {{ macros::input(label='', name='max_people', type='hidden', value=-1) }} {{ macros::input(label='', name='name', type='hidden', value=event.name) }} - {{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }} {{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }} {{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }} {{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }} @@ -398,7 +397,7 @@

Ausfahrt absagen

- {{ macros::input(label='', name='max_people', type='hidden', value=0) }} + {{ macros::input(label='', name='max_people', type='hidden', value=-1) }} {{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }} {{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }} {{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }} From 5cd75ed8c8c18dff4ec0eae76a780926235d64c5 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 18 Apr 2025 23:24:03 +0200 Subject: [PATCH 12/20] also be able to cancel trips (not only events) --- src/model/mod.rs | 4 ++-- src/model/trip.rs | 20 ++++++++++---------- src/model/tripdetails.rs | 4 ++-- src/model/usertrip.rs | 4 ++-- templates/planned.html.tera | 10 +++++++--- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/model/mod.rs b/src/model/mod.rs index b9d45d3..8cc6613 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -7,7 +7,7 @@ use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use self::{ event::{Event, EventWithDetails}, - trip::{Trip, TripWithUserAndType}, + trip::{Trip, TripWithDetails}, waterlevel::Waterlevel, weather::Weather, }; @@ -45,7 +45,7 @@ pub mod weather; pub struct Day { day: NaiveDate, events: Vec, - trips: Vec, + trips: Vec, is_pinned: bool, regular_sees_this_day: bool, max_waterlevel: Option, diff --git a/src/model/trip.rs b/src/model/trip.rs index 7565ea0..9b14133 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -30,11 +30,12 @@ pub struct Trip { } #[derive(Serialize, Debug)] -pub struct TripWithUserAndType { +pub struct TripWithDetails { #[serde(flatten)] pub trip: Trip, pub rower: Vec, trip_type: Option, + cancelled: bool, } pub struct TripUpdate<'a> { @@ -52,7 +53,7 @@ impl<'a> TripUpdate<'a> { } } -impl TripWithUserAndType { +impl TripWithDetails { pub async fn from(db: &SqlitePool, trip: Trip) -> Self { let mut trip_type = None; if let Some(trip_type_id) = trip.trip_type_id { @@ -60,8 +61,9 @@ impl TripWithUserAndType { } Self { rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await, - trip, trip_type, + cancelled: trip.is_cancelled(), + trip, } } } @@ -268,12 +270,12 @@ WHERE trip.id=? } } - pub async fn get_for_today(db: &SqlitePool) -> Vec { + pub async fn get_for_today(db: &SqlitePool) -> Vec { let today = Local::now().date_naive(); Self::get_for_day(db, today).await } - pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { + pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { let day = format!("{day}"); let trips = sqlx::query_as!( Trip, @@ -292,7 +294,7 @@ WHERE day=? let mut ret = Vec::new(); for trip in trips { - ret.push(TripWithUserAndType::from(db, trip).await); + ret.push(TripWithDetails::from(db, trip).await); } ret } @@ -336,9 +338,7 @@ WHERE day=? .unwrap(); //Okay, as trip_details can only be created with proper DB backing if update.cancelled() && !was_already_cancelled { - let rowers = TripWithUserAndType::from(db, update.trip.clone()) - .await - .rower; + let rowers = TripWithDetails::from(db, update.trip.clone()).await.rower; for user in rowers { if let Some(user) = User::find_by_name(db, &user.name).await { let notes = match update.notes { @@ -462,7 +462,7 @@ WHERE day=? pub(crate) async fn get_pinned_for_day( db: &sqlx::Pool, day: NaiveDate, - ) -> Vec { + ) -> Vec { let mut trips = Self::get_for_day(db, day).await; trips.retain(|e| e.trip.always_show); trips diff --git a/src/model/tripdetails.rs b/src/model/tripdetails.rs index 46b74f5..e19a2b7 100644 --- a/src/model/tripdetails.rs +++ b/src/model/tripdetails.rs @@ -6,7 +6,7 @@ use sqlx::{FromRow, SqlitePool}; use super::{ notification::Notification, - trip::{Trip, TripWithUserAndType}, + trip::{Trip, TripWithDetails}, triptype::TripType, }; @@ -138,7 +138,7 @@ WHERE day = ? AND planned_starting_time = ? // This trip_details belongs to a planned_event, no need to do anything continue; }; - let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await; + let pot_coxes = TripWithDetails::from(db, trip.clone()).await; let pot_coxes = pot_coxes.rower; for user in pot_coxes { let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); diff --git a/src/model/usertrip.rs b/src/model/usertrip.rs index 4ea4341..9f14ba6 100644 --- a/src/model/usertrip.rs +++ b/src/model/usertrip.rs @@ -3,7 +3,7 @@ use sqlx::{FromRow, SqlitePool}; use super::{ notification::Notification, - trip::{Trip, TripWithUserAndType}, + trip::{Trip, TripWithDetails}, tripdetails::TripDetails, user::{SteeringUser, User}, }; @@ -158,7 +158,7 @@ impl UserTrip { .unwrap() .cancelled() { - let trip = TripWithUserAndType::from(db, trip.clone()).await; + let trip = TripWithDetails::from(db, trip.clone()).await; if trip.rower.len() == 1 { trip_to_delete = Some(trip.trip); } diff --git a/templates/planned.html.tera b/templates/planned.html.tera index e315430..a3c2fac 100644 --- a/templates/planned.html.tera +++ b/templates/planned.html.tera @@ -240,7 +240,11 @@ {{ macros::input(label='Titel', name='name', type='input', value=event.name) }} - {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='0') }} + {% if event.cancelled %} + + {% else %} + {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='0') }} + {% endif %} {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }} {{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }} {{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }} @@ -300,7 +304,7 @@ {% if trip.always_show and not day.regular_sees_this_day %} 🔮 {% endif -%} - {% if trip.max_people == 0 %} + {% if trip.cancelled %} ⚠ {{ trip.planned_starting_time }} Uhr @@ -322,7 +326,7 @@ {% endif %}
{% else %} - {% if trip.max_people == 0 %} + {% if trip.cancelled %} Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen. {% else %}
From b4a22820e7a868429d25b2c0c75582eb25bf2154 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 18 Apr 2025 23:48:08 +0200 Subject: [PATCH 14/20] tests are not using magic values as well... --- frontend/tests/cox.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index 3ef645e..74cb6f6 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -126,9 +126,7 @@ test.describe("cox can edit trips", () => { await expect(sharedPage.locator("#sidebar")).toContainText( "Freie Plätze: 3", ); - await sharedPage.getByRole("spinbutton").click(); - await sharedPage.getByRole("spinbutton").fill("0"); - await sharedPage.getByRole("button", { name: "Speichern" }).click(); + await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click(); await expect(sharedPage.locator("body")).toContainText( "Ausfahrt erfolgreich aktualisiert.", ); From 9b31ea981ad63ea7a774f13f5f102a5a5262a356 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Sat, 19 Apr 2025 00:21:51 +0200 Subject: [PATCH 15/20] hack in frontend test not working, as we can' hack a cancellation with setting rowers=0 --- frontend/tests/cox.spec.ts | 59 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index 74cb6f6..89da881 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -121,16 +121,69 @@ test.describe("cox can edit trips", () => { }); test("call off trip", async () => { + // Someone registers... + await sharedPage.goto("/auth/logout"); + await sharedPage.goto("/auth"); + await sharedPage.getByPlaceholder("Name").click(); + await sharedPage.getByPlaceholder("Name").fill("rower"); + await sharedPage.getByPlaceholder("Name").press("Tab"); + await sharedPage.getByPlaceholder("Passwort").fill("rower"); + await sharedPage.getByPlaceholder("Passwort").press("Enter"); + await sharedPage.goto("/planned"); + await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); + + + // Login as cox again + await sharedPage.goto("/auth/logout"); + await sharedPage.goto("/auth"); + await sharedPage.getByPlaceholder("Name").click(); + await sharedPage.getByPlaceholder("Name").fill("cox"); + await sharedPage.getByPlaceholder("Name").press("Tab"); + await sharedPage.getByPlaceholder("Passwort").fill("cox"); + await sharedPage.getByPlaceholder("Passwort").press("Enter"); + + await sharedPage.goto("/planned"); + + + // ... now I can cancel trip await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await expect(sharedPage.locator("#sidebar")).toContainText( - "Freie Plätze: 3", - ); await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click(); await expect(sharedPage.locator("body")).toContainText( "Ausfahrt erfolgreich aktualisiert.", ); await expect(sharedPage.locator("body")).toContainText("(Absage cox)"); + + + // Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails + await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); + await sharedPage.getByRole("spinbutton").click(); + await sharedPage.getByRole("spinbutton").fill("3"); + await sharedPage.getByRole("button", { name: "Speichern" }).click(); + + + + // deregistering + await sharedPage.goto("/auth/logout"); + await sharedPage.goto("/auth"); + await sharedPage.getByPlaceholder("Name").click(); + await sharedPage.getByPlaceholder("Name").fill("rower"); + await sharedPage.getByPlaceholder("Name").press("Tab"); + await sharedPage.getByPlaceholder("Passwort").fill("rower"); + await sharedPage.getByPlaceholder("Passwort").press("Enter"); + + await sharedPage.goto("/planned"); + await sharedPage.getByRole('link', { name: 'Abmelden' }).click(); + + + // now cox can delete trip again in afterAll + await sharedPage.goto("/auth/logout"); + await sharedPage.goto("/auth"); + await sharedPage.getByPlaceholder("Name").click(); + await sharedPage.getByPlaceholder("Name").fill("cox"); + await sharedPage.getByPlaceholder("Name").press("Tab"); + await sharedPage.getByPlaceholder("Passwort").fill("cox"); + await sharedPage.getByPlaceholder("Passwort").press("Enter"); }); test.afterAll(async () => { From 3eb84ce46b487f8b766141df13d71cfda2907a04 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Sat, 19 Apr 2025 09:19:11 +0200 Subject: [PATCH 16/20] document nextcloud integration, for future nextcloud setups --- doc/nextcloud-notes.md | 94 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 doc/nextcloud-notes.md diff --git a/doc/nextcloud-notes.md b/doc/nextcloud-notes.md new file mode 100644 index 0000000..b820e4b --- /dev/null +++ b/doc/nextcloud-notes.md @@ -0,0 +1,94 @@ +# Nextcloud integration + +- Based on [this plugin](https://github.com/nextcloud/user_external) +- Install that plugin via web +- Connect to server, enter nextcloud-docker-image: `docker exec -it nextcloud-aio-nextcloud bash` +- Adapt `/var/www/html/custom_apps/user_external/lib/BasicAuth.php` to switch from BasicAuth to RowtAuth: +```php + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\UserExternal; + +class BasicAuth extends Base { + private $authUrl; + + public function __construct($authUrl) { + parent::__construct($authUrl); + $this->authUrl = $authUrl; + } + + /** + * Check if the password is correct without logging in the user + * + * @param string $uid The username + * @param string $password The password + * + * @return true/false + */ + public function checkPassword($uid, $password) { + // Prepare POST data with credentials + $postData = http_build_query([ + 'name' => $uid, + 'password' => $password + ]); + + // Create context with POST method + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-Type: application/x-www-form-urlencoded', + 'content' => $postData, + 'follow_location' => 0 + ] + ]); + + // Get the content of the response + $content = @file_get_contents($this->authUrl, false, $context); + + if ($content === false) { + \OC::$server->getLogger()->error( + 'ERROR: Failed to get content from Auth Url: '.$this->authUrl, + ['app' => 'user_external'] + ); + return false; + } + + // Check if the content is "SUCC" + if (trim($content) === "SUCC") { + $this->storeUser($uid); + return $uid; + } + + return false; + } +} +``` +- In `/var/www/html/config/config.php` add this: +``` + 'user_backends' => + array ( + 0 => + array ( + 'class' => '\\OCA\\UserExternal\\BasicAuth', + 'arguments' => + array ( + 0 => 'https://app.rudernlinz.at/nxauth', + ), + ), + ), +``` +- In `/var/www/html/config/config.php` add this `'skeletondirectory' => '',` to disable default folders for new users +- To automatically add users to a group (e.g. `vorstand`), use the `Auto Groups` plugin +- Shared folders are not shared with new members due to [this bug](https://github.com/nextcloud/server/issues/25062#issuecomment-766445043) + - Find DB config: `docker exec nextcloud-aio-database env | grep POSTGRES` + - Workaround: Connect to docker-db: `docker exec -it nextcloud-aio-database bash` + - Connect to db: `psql -U nextcloud -d nextcloud_database` + - (with `\l` you see all dbs) + - Connect to nextcloud db: `\c nextcloud_database` + - Do query from issue: `UPDATE oc_share SET accepted = 1 WHERE share_type = 1;` From f289c7b6d78a69732a50f9f82190fdbc39489569 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Sat, 19 Apr 2025 21:29:17 +0200 Subject: [PATCH 17/20] no need to show rower box, if no rower can particiapte --- templates/planned.html.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/planned.html.tera b/templates/planned.html.tera index b4c11a6..fee4477 100644 --- a/templates/planned.html.tera +++ b/templates/planned.html.tera @@ -217,7 +217,7 @@ {% set amount_cur_rower = event.rower | length %} {% if event.cancelled %} {{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }} - {% else %} + {% elif event.max_people > 0 %} {{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }} {% endif %} {# --- END List Rowers --- #} From 47b46cf41d0e6b15448f2369c3e2e8efdd0b0733 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Mon, 28 Apr 2025 22:20:06 +0200 Subject: [PATCH 18/20] format dtstart according to ics standard -> leading zero --- src/model/event.rs | 8 +++++++- src/model/trip.rs | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/model/event.rs b/src/model/event.rs index 90eb9c0..6258efb 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -457,10 +457,16 @@ WHERE trip_details.id=? format!("event-{}@rudernlinz.at", self.id), "19900101T180000", ); + let time_str = self.planned_starting_time.replace(':', ""); + let formatted_time = if time_str.len() == 3 { + format!("0{}", time_str) + } else { + time_str.clone() // TODO: remove again + }; vevent.push(DtStart::new(format!( "{}T{}00", self.day.replace('-', ""), - self.planned_starting_time.replace(':', "") + formatted_time ))); let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") diff --git a/src/model/trip.rs b/src/model/trip.rs index 9b14133..98dd878 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -148,10 +148,17 @@ WHERE trip_details.id=? pub(crate) async fn get_vevent(self, user: &User) -> ics::Event { let mut vevent = ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000"); + let time_str = self.planned_starting_time.replace(':', ""); + let formatted_time = if time_str.len() == 3 { + format!("0{}", time_str) + } else { + time_str + }; + vevent.push(DtStart::new(format!( "{}T{}00", self.day.replace('-', ""), - self.planned_starting_time.replace(':', "") + formatted_time ))); let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") From c548bf211baf58099c7660a2aebbc71f48190462 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Mon, 28 Apr 2025 22:39:45 +0200 Subject: [PATCH 19/20] fix ci --- frontend/tests/cox.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index d238e96..23e0508 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -129,7 +129,7 @@ test.describe("cox can edit trips", () => { await sharedPage.getByPlaceholder("Passwort").fill("rower"); await sharedPage.getByPlaceholder("Passwort").press("Enter"); - await sharedPage.goto("/planned"); + await sharedPage.goto("/"); await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); @@ -142,7 +142,7 @@ test.describe("cox can edit trips", () => { await sharedPage.getByPlaceholder("Passwort").fill("cox"); await sharedPage.getByPlaceholder("Passwort").press("Enter"); - await sharedPage.goto("/planned"); + await sharedPage.goto("/"); // ... now I can cancel trip @@ -171,7 +171,7 @@ test.describe("cox can edit trips", () => { await sharedPage.getByPlaceholder("Passwort").fill("rower"); await sharedPage.getByPlaceholder("Passwort").press("Enter"); - await sharedPage.goto("/planned"); + await sharedPage.goto("/"); await sharedPage.getByRole('link', { name: 'Abmelden' }).click(); From 39e1c4432a2485ff84fdb7fc9c7740becc21a3b1 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Mon, 28 Apr 2025 22:40:48 +0200 Subject: [PATCH 20/20] remove compiler warnings, Fixes #1 --- src/model/event.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model/event.rs b/src/model/event.rs index 1ff1141..00c2ad9 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -11,7 +11,6 @@ use sqlx::{FromRow, Row, SqlitePool}; use super::{ log::Log, notification::Notification, - role::Role, tripdetails::TripDetails, triptype::TripType, user::{EventUser, User},