Merge pull request 'more-activities' (#1035) from more-activities into staging
Reviewed-on: #1035
This commit was merged in pull request #1035.
	This commit is contained in:
		| @@ -17,10 +17,31 @@ pub struct Activity { | ||||
|     pub keep_until: Option<NaiveDateTime>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| pub struct ActivityWithDetails { | ||||
|     #[serde(flatten)] | ||||
|     pub(crate) activity: Activity, | ||||
|     keep_until_days: Option<i64>, | ||||
| } | ||||
|  | ||||
| impl From<Activity> for ActivityWithDetails { | ||||
|     fn from(activity: Activity) -> Self { | ||||
|         let keep_until_days = activity.keep_until.map(|keep_until| { | ||||
|             let now = Utc::now().naive_utc(); | ||||
|             let duration = keep_until.signed_duration_since(now); | ||||
|             duration.num_days() | ||||
|         }); | ||||
|  | ||||
|         Self { | ||||
|             keep_until_days, | ||||
|             activity, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // TODO: add `reason` as additional db field, to be able to query and show this to the users | ||||
| pub enum Reason<'a> { | ||||
|     // `User` tried to login with `String` as UserAgent | ||||
|     SuccLogin(&'a User, String), | ||||
|     Auth(ReasonAuth<'a>), | ||||
|     // `User` changed the data of `User`, explanation in `String` | ||||
|     UserDataChange(&'a ManageUserUser, &'a User, String), | ||||
|     // New Note for User | ||||
| @@ -30,11 +51,7 @@ pub enum Reason<'a> { | ||||
| impl From<Reason<'_>> for ActivityBuilder { | ||||
|     fn from(value: Reason<'_>) -> Self { | ||||
|         match value { | ||||
|             Reason::SuccLogin(user, agent) => { | ||||
|                 Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})")) | ||||
|                     .relevant_for_user(user) | ||||
|                     .keep_until_days(7) | ||||
|             } | ||||
|             Reason::Auth(auth) => auth.into(), | ||||
|             Reason::UserDataChange(changed_by, changed_user, explanation) => Self::new(&format!( | ||||
|                 "{changed_by} hat die Daten von {changed_user} aktualisiert: {explanation}" | ||||
|             )) | ||||
| @@ -46,6 +63,43 @@ impl From<Reason<'_>> for ActivityBuilder { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum ReasonAuth<'a> { | ||||
|     // `User` tried to login with `String` as UserAgent | ||||
|     SuccLogin(&'a User, String), | ||||
|     // `User` tried to login which was already deleted | ||||
|     DeletedUserLogin(&'a User), | ||||
|     // `User` tried to login, supplied wrong PW | ||||
|     WrongPw(&'a User), | ||||
| } | ||||
|  | ||||
| impl<'a> From<ReasonAuth<'a>> for Reason<'a> { | ||||
|     fn from(auth_reason: ReasonAuth<'a>) -> Self { | ||||
|         Reason::Auth(auth_reason) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ReasonAuth<'_>> for ActivityBuilder { | ||||
|     fn from(value: ReasonAuth<'_>) -> Self { | ||||
|         match value { | ||||
|             ReasonAuth::SuccLogin(user, agent) => { | ||||
|                 Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})")) | ||||
|                     .relevant_for_user(user) | ||||
|                     .keep_until_days(7) | ||||
|             } | ||||
|             ReasonAuth::DeletedUserLogin(user) => Self::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." | ||||
|             )) | ||||
|             .relevant_for_user(user) | ||||
|             .keep_until_days(30), | ||||
|             ReasonAuth::WrongPw(user) => Self::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben." | ||||
|             )) | ||||
|             .relevant_for_user(user) | ||||
|             .keep_until_days(7), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct ActivityBuilder { | ||||
|     text: String, | ||||
|     relevant_for: String, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| use std::ops::DerefMut; | ||||
|  | ||||
| use chrono::NaiveDateTime; | ||||
| use rocket::serde::{Deserialize, Serialize}; | ||||
| use rocket::FromForm; | ||||
| use rocket::serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use crate::model::boathouse::Boathouse; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use std::ops::DerefMut; | ||||
|  | ||||
| use serde::Serialize; | ||||
| use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult}; | ||||
|  | ||||
| use super::user::User; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| use std::{error::Error, fs}; | ||||
|  | ||||
| use lettre::{ | ||||
|     message::{header::ContentType, Attachment, MultiPart, SinglePart}, | ||||
|     transport::smtp::authentication::Credentials, | ||||
|     Address, Message, SmtpTransport, Transport, | ||||
|     message::{Attachment, MultiPart, SinglePart, header::ContentType}, | ||||
|     transport::smtp::authentication::Credentials, | ||||
| }; | ||||
| use sqlx::{Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| use super::User; | ||||
| use crate::{ | ||||
|     model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, | ||||
|     FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, TRIAL_ROWING, | ||||
|     TRIAL_ROWING_REDUCED, UNTERSTUETZEND, | ||||
|     BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, | ||||
|     REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED, | ||||
|     UNTERSTUETZEND, model::family::Family, | ||||
| }; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use serde::Serialize; | ||||
|   | ||||
| @@ -1,20 +1,21 @@ | ||||
| use std::{fmt::Display, ops::DerefMut}; | ||||
|  | ||||
| use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | ||||
| use argon2::{Argon2, PasswordHasher, password_hash::SaltString}; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use log::info; | ||||
| use rocket::async_trait; | ||||
| use rocket::{ | ||||
|     Request, | ||||
|     http::{Cookie, Status}, | ||||
|     request::{FromRequest, Outcome}, | ||||
|     time::{Duration, OffsetDateTime}, | ||||
|     Request, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::activity::ActivityBuilder; | ||||
| use super::activity::{ActivityBuilder, ReasonAuth}; | ||||
| use super::{ | ||||
|     Day, | ||||
|     log::Log, | ||||
|     logbook::Logbook, | ||||
|     mail::Mail, | ||||
| @@ -23,7 +24,6 @@ use super::{ | ||||
|     role::Role, | ||||
|     stat::Stat, | ||||
|     tripdetails::TripDetails, | ||||
|     Day, | ||||
| }; | ||||
| use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; | ||||
| use scheckbuch::ScheckbuchUser; | ||||
| @@ -465,53 +465,29 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|     pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> { | ||||
|         let name = name.trim().to_lowercase(); // just to make sure... | ||||
|         let Some(user) = User::find_by_name(db, &name).await else { | ||||
|             if ![ | ||||
|                 "n-sageder", | ||||
|                 "p-hofer", | ||||
|                 "marie-birner", | ||||
|                 "daniel-kortschak", | ||||
|                 "rudernlinz", | ||||
|                 "m-birner", | ||||
|                 "s-sollberger", | ||||
|                 "d-kortschak", | ||||
|                 "wwwadmin", | ||||
|                 "wadminw", | ||||
|                 "admin", | ||||
|                 "m sageder", | ||||
|                 "d kortschak", | ||||
|                 "a almousa", | ||||
|                 "p hofer", | ||||
|                 "s sollberger", | ||||
|                 "n sageder", | ||||
|                 "wp-system", | ||||
|                 "s.sollberger", | ||||
|                 "m.birner", | ||||
|                 "m-sageder", | ||||
|                 "a-almousa", | ||||
|                 "m.sageder", | ||||
|                 "n.sageder", | ||||
|                 "a.almousa", | ||||
|                 "p.hofer", | ||||
|                 "philipp-hofer", | ||||
|                 "d.kortschak", | ||||
|                 "[login]", | ||||
|             ] | ||||
|             .contains(&name.as_str()) | ||||
|             { | ||||
|                 Log::create(db, format!("Username ({name}) not found (tried to login)")).await; | ||||
|             } | ||||
|             Log::create(db, format!("Username ({name}) not found (tried to login)")).await; | ||||
|             return Err(LoginError::InvalidAuthenticationCombo); // Username not found | ||||
|         }; | ||||
|  | ||||
|         if user.deleted { | ||||
|             ActivityBuilder::new(&format!( | ||||
|             if let Some(board) = Role::find_by_name(db, "Vorstand").await { | ||||
|                 Notification::create_for_role( | ||||
|                     db, | ||||
|                     &board, | ||||
|                     &format!( | ||||
|                 "User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." | ||||
|             )) | ||||
|             .relevant_for_user(&user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|             ), | ||||
|                     "Fehlgeschlagener Login", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|             } | ||||
|             ActivityBuilder::from(ReasonAuth::DeletedUserLogin(&user)) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|             return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has | ||||
|                                                                 //been deleted | ||||
|             //been deleted | ||||
|         } | ||||
|  | ||||
|         if let Some(user_pw) = user.pw.as_ref() { | ||||
| @@ -519,12 +495,9 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             if password_hash == user_pw { | ||||
|                 return Ok(user); | ||||
|             } | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben." | ||||
|             )) | ||||
|             .relevant_for_user(&user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|             ActivityBuilder::from(ReasonAuth::WrongPw(&user)) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|             Err(LoginError::InvalidAuthenticationCombo) | ||||
|         } else { | ||||
|             info!("User {name} has no PW set"); | ||||
| @@ -617,9 +590,9 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|     pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 { | ||||
|         if self.allowed_to_steer(db).await { | ||||
|             let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok, | ||||
|                                                                                              //december | ||||
|                                                                                              //has 31 | ||||
|                                                                                              //days | ||||
|             //december | ||||
|             //has 31 | ||||
|             //days | ||||
|             let days_left_in_year = end_of_year | ||||
|                 .signed_duration_since(Local::now().date_naive()) | ||||
|                 .num_days() | ||||
| @@ -628,9 +601,9 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             if days_left_in_year <= 31 { | ||||
|                 let end_of_next_year = | ||||
|                     NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok, | ||||
|                                                                                        //december | ||||
|                                                                                        //has 31 | ||||
|                                                                                        //days | ||||
|                 //december | ||||
|                 //has 31 | ||||
|                 //days | ||||
|                 end_of_next_year | ||||
|                     .signed_duration_since(Local::now().date_naive()) | ||||
|                     .num_days() | ||||
| @@ -862,8 +835,8 @@ special_user!(SteeringUser, +"cox", +"Bootsführer"); | ||||
| special_user!(AdminUser, +"admin"); | ||||
| special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied"); | ||||
| special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); // TODO: | ||||
|                                                                                        // remove -> | ||||
|                                                                                        // RegularUser | ||||
| // remove -> | ||||
| // RegularUser | ||||
| special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); | ||||
| special_user!(VorstandUser, +"admin", +"Vorstand"); | ||||
| special_user!(EventUser, +"manage_events"); | ||||
| @@ -977,17 +950,21 @@ mod test { | ||||
|     #[sqlx::test] | ||||
|     fn wrong_pw() { | ||||
|         let pool = testdb!(); | ||||
|         assert!(User::login(&pool, "admin".into(), "admi".into()) | ||||
|             .await | ||||
|             .is_err()); | ||||
|         assert!( | ||||
|             User::login(&pool, "admin".into(), "admi".into()) | ||||
|                 .await | ||||
|                 .is_err() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn wrong_username() { | ||||
|         let pool = testdb!(); | ||||
|         assert!(User::login(&pool, "admi".into(), "admin".into()) | ||||
|             .await | ||||
|             .is_err()); | ||||
|         assert!( | ||||
|             User::login(&pool, "admi".into(), "admin".into()) | ||||
|                 .await | ||||
|                 .is_err() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
| @@ -1007,9 +984,11 @@ mod test { | ||||
|         let pool = testdb!(); | ||||
|         let user = User::find_by_id(&pool, 1).await.unwrap(); | ||||
|  | ||||
|         assert!(User::login(&pool, "admin".into(), "abc".into()) | ||||
|             .await | ||||
|             .is_err()); | ||||
|         assert!( | ||||
|             User::login(&pool, "admin".into(), "abc".into()) | ||||
|                 .await | ||||
|                 .is_err() | ||||
|         ); | ||||
|  | ||||
|         user.update_pw(&pool, "abc".into()).await; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| use super::{ManageUserUser, User}; | ||||
| use crate::{ | ||||
|     NonEmptyString, | ||||
|     model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, | ||||
|     special_user, NonEmptyString, | ||||
|     special_user, | ||||
| }; | ||||
| use chrono::NaiveDate; | ||||
| use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt}; | ||||
|   | ||||
| @@ -2,12 +2,13 @@ use super::foerdernd::FoerderndUser; | ||||
| use super::regular::RegularUser; | ||||
| use super::unterstuetzend::UnterstuetzendUser; | ||||
| use super::{ManageUserUser, User}; | ||||
| use crate::NonEmptyString; | ||||
| use crate::model::activity::ActivityBuilder; | ||||
| use crate::model::role::Role; | ||||
| use crate::NonEmptyString; | ||||
| use crate::{ | ||||
|     SCHECKBUCH, | ||||
|     model::{mail::Mail, notification::Notification}, | ||||
|     special_user, SCHECKBUCH, | ||||
|     special_user, | ||||
| }; | ||||
| use chrono::NaiveDate; | ||||
| use rocket::async_trait; | ||||
|   | ||||
| @@ -4,9 +4,9 @@ use super::scheckbuch::ScheckbuchUser; | ||||
| use super::schnupperinterest::SchnupperInterestUser; | ||||
| use super::unterstuetzend::UnterstuetzendUser; | ||||
| use super::{ManageUserUser, User}; | ||||
| use crate::NonEmptyString; | ||||
| use crate::model::activity::ActivityBuilder; | ||||
| use crate::model::role::Role; | ||||
| use crate::NonEmptyString; | ||||
| use crate::{ | ||||
|     model::{mail::Mail, notification::Notification}, | ||||
|     special_user, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use csv::ReaderBuilder; | ||||
| use rocket::{form::Form, get, post, routes, FromForm, Route, State}; | ||||
| use rocket_dyn_templates::{context, Template}; | ||||
| use rocket::{FromForm, Route, State, form::Form, get, post, routes}; | ||||
| use rocket_dyn_templates::{Template, context}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{activity::Activity, role::Role, user::AdminUser}; | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| use crate::{ | ||||
|     model::{ | ||||
|         activity::Activity, | ||||
|         activity::{Activity, ActivityWithDetails}, | ||||
|         family::Family, | ||||
|         log::Log, | ||||
|         logbook::Logbook, | ||||
|         mail::valid_mails, | ||||
|         role::Role, | ||||
|         user::{ | ||||
|             AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, | ||||
|             UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, | ||||
|             clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member, | ||||
|             regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser, | ||||
|             schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser, | ||||
|             AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, | ||||
|             UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, | ||||
|         }, | ||||
|     }, | ||||
|     tera::Config, | ||||
| @@ -19,6 +19,7 @@ use crate::{ | ||||
| use chrono::NaiveDate; | ||||
| use futures::future::join_all; | ||||
| use rocket::{ | ||||
|     FromForm, Request, Route, State, | ||||
|     form::Form, | ||||
|     fs::TempFile, | ||||
|     get, | ||||
| @@ -26,9 +27,9 @@ use rocket::{ | ||||
|     post, | ||||
|     request::{FlashMessage, FromRequest, Outcome}, | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, FromForm, Request, Route, State, | ||||
|     routes, | ||||
| }; | ||||
| use rocket_dyn_templates::{tera::Context, Template}; | ||||
| use rocket_dyn_templates::{Template, tera::Context}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| // Custom request guard to extract the Referer header | ||||
| @@ -141,7 +142,11 @@ async fn view( | ||||
|  | ||||
|     let member = Member::from(db, user.clone()).await; | ||||
|     let fee = user.fee(db).await; | ||||
|     let activities = Activity::for_user(db, &user).await; | ||||
|     let activities: Vec<ActivityWithDetails> = Activity::for_user(db, &user) | ||||
|         .await | ||||
|         .into_iter() | ||||
|         .map(Into::into) | ||||
|         .collect(); | ||||
|     let financial = Role::all_cluster(db, "financial").await; | ||||
|     let user_financial = user.financial(db).await; | ||||
|     let skill = Role::all_cluster(db, "skill").await; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| use rocket::{ | ||||
|     FromForm, Request, Route, State, | ||||
|     form::Form, | ||||
|     get, | ||||
|     http::{Cookie, CookieJar}, | ||||
| @@ -8,13 +9,12 @@ use rocket::{ | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, | ||||
|     time::{Duration, OffsetDateTime}, | ||||
|     FromForm, Request, Route, State, | ||||
| }; | ||||
| use rocket_dyn_templates::{context, tera, Template}; | ||||
| use rocket_dyn_templates::{Template, context, tera}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{ | ||||
|     activity::{self, ActivityBuilder}, | ||||
|     activity::{self, ActivityBuilder, ReasonAuth}, | ||||
|     log::Log, | ||||
|     user::{LoginError, User}, | ||||
| }; | ||||
| @@ -83,7 +83,7 @@ async fn login( | ||||
|  | ||||
|     cookies.add_private(Cookie::new("loggedin_user", format!("{}", user.id))); | ||||
|  | ||||
|     ActivityBuilder::from(activity::Reason::SuccLogin(&user, agent.0)) | ||||
|     ActivityBuilder::from(ReasonAuth::SuccLogin(&user, agent.0)) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -411,7 +411,9 @@ | ||||
|                         <ul class="list-disc ms-4"> | ||||
|                             {% for activity in activities %} | ||||
|                                 <li> | ||||
|                                     <strong>{{ activity.created_at | date(format="%d. %m. %Y") }}:</strong> <small>{{ activity.text }}</small> | ||||
|                                     <strong>{{ activity.created_at | date(format="%d. %m. %Y") }}:</strong> <small>{{ activity.text }} | ||||
|                                         {% if activity.keep_until_days %}(⏳ {{ activity.keep_until_days }} Tage){% endif %} | ||||
|                                     </small> | ||||
|                                 </li> | ||||
|                             {% else %} | ||||
|                                 <li>Noch keine Aktivität... Stay tuned 😆</li> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user