use std::ops::{Deref, DerefMut}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use chrono::{Datelike, Local, NaiveDate}; use log::info; use rocket::{ async_trait, http::{Cookie, Status}, request::{self, FromRequest, Outcome}, time::{Duration, OffsetDateTime}, tokio::io::AsyncReadExt, Request, }; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{ family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat, tripdetails::TripDetails, Day, }; use crate::tera::admin::user::UserEditForm; const RENNRUDERBEITRAG: i32 = 11000; const BOAT_STORAGE: i32 = 4500; const FAMILY_TWO: i32 = 30000; const FAMILY_THREE_OR_MORE: i32 = 35000; const STUDENT_OR_PUPIL: i32 = 8000; const REGULAR: i32 = 22000; const UNTERSTUETZEND: i32 = 2500; const FOERDERND: i32 = 8500; pub const SCHECKBUCH: i32 = 3000; #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub struct User { pub id: i64, pub name: String, pub pw: Option, pub dob: Option, pub weight: Option, pub sex: Option, pub deleted: bool, pub last_access: Option, pub member_since_date: Option, pub birthdate: Option, pub mail: Option, pub nickname: Option, pub notes: Option, pub phone: Option, pub address: Option, pub family_id: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct UserWithDetails { #[serde(flatten)] pub user: User, pub amount_unread_notifications: i32, pub on_water: bool, pub roles: Vec, } impl UserWithDetails { pub async fn from_user(user: User, db: &SqlitePool) -> Self { Self { on_water: user.on_water(db).await, roles: user.roles(db).await, amount_unread_notifications: user.amount_unread_notifications(db).await, user, } } } #[derive(Debug)] pub enum LoginError { InvalidAuthenticationCombo, UserNotFound, UserDeleted, NotLoggedIn, NotAnAdmin, NotACox, NotATech, GuestNotAllowed, NoPasswordSet(Box), DeserializationError, } #[derive(Debug, Serialize)] pub struct Fee { pub sum_in_cents: i32, pub parts: Vec<(String, i32)>, pub name: String, pub user_ids: String, pub paid: bool, pub users: Vec, } impl Default for Fee { fn default() -> Self { Self::new() } } impl Fee { pub fn new() -> Self { Self { sum_in_cents: 0, name: "".into(), parts: Vec::new(), user_ids: "".into(), users: Vec::new(), paid: false, } } pub fn add(&mut self, desc: String, price_in_cents: i32) { self.sum_in_cents += price_in_cents; self.parts.push((desc, price_in_cents)); } pub fn add_person(&mut self, user: &User) { if !self.name.is_empty() { self.name.push_str(" + "); self.user_ids.push('&'); } self.name.push_str(&user.name); self.user_ids.push_str(&format!("user_ids[]={}", user.id)); self.users.push(user.clone()); } pub fn paid(&mut self) { self.paid = true; } pub fn merge(&mut self, fee: Fee) { for (desc, price_in_cents) in fee.parts { self.add(desc, price_in_cents); } } } impl User { pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { let Some(mail) = &self.mail else { return Err(format!( "Could not send welcome mail, because user {} has no email address", self.name )); }; if self.has_role(db, "Donau Linz").await { self.send_welcome_mail_full_member(db, mail, smtp_pw) .await?; } else if self.has_role(db, "scheckbuch").await { self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await?; } else if self.has_role(db, "schnupperant").await { self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?; } else { return Err(format!( "Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group", self.name )); } Log::create( db, format!("Willkommensemail wurde an {} versandt", self.name), ) .await; Ok(()) } async fn send_welcome_mail_schnupper( &self, db: &SqlitePool, mail: &str, smtp_pw: &str, ) -> Result<(), String> { // 2 things to do: // 1. Send mail to user Mail::send_single( db, mail, "Schnupperrudern beim ASKÖ Ruderverein Donau Linz", format!( "Hallo {0}, es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden. Liebe Grüße, Philipp", self.name), smtp_pw, ).await?; // 2. Notify all coxes let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap(); Notification::create_for_role( db, &coxes, &format!( "Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.", self.name ), "Neue(r) Schnupperteilnehmer:in ", None, None, ) .await; Ok(()) } async fn send_welcome_mail_scheckbuch( &self, db: &SqlitePool, mail: &str, smtp_pw: &str, ) -> Result<(), String> { // 2 things to do: // 1. Send mail to user Mail::send_single( db, mail, "ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich", format!( "Hallo {0}, herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]). Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-) Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen. Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Riemen- & Dollenbruch, ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100), smtp_pw, ).await?; // 2. Notify all coxes let coxes = Role::find_by_name(db, "cox").await.unwrap(); Notification::create_for_role( db, &coxes, &format!( "Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.", self.name ), "Neues Scheckbuch", None,None ) .await; Ok(()) } async fn send_welcome_mail_full_member( &self, db: &SqlitePool, mail: &str, smtp_pw: &str, ) -> Result<(), String> { // 2 things to do: // 1. Send mail to user Mail::send_single( db, mail, "Willkommen im ASKÖ Ruderverein Donau Linz!", format!( "Hallo {0}, herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen. Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung. Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden. Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst. Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Riemen- & Dollenbruch ASKÖ Ruderverein Donau Linz", self.name), smtp_pw, ).await?; // 2. Notify all coxes let coxes = Role::find_by_name(db, "cox").await.unwrap(); Notification::create_for_role( db, &coxes, &format!( "Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}", self.member_since_date.clone().unwrap(), self.name ), "Neues Vereinsmitglied", None, None, ) .await; Ok(()) } pub async fn fee(&self, db: &SqlitePool) -> Option { if !self.has_role(db, "Donau Linz").await { return None; } if self.deleted { return None; } let mut fee = Fee::new(); if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { for member in family.members(db).await { fee.add_person(&member); if member.has_role(db, "paid").await { fee.paid(); } fee.merge(member.fee_without_families(db).await); } if family.amount_family_members(db).await > 2 { fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); } else { fee.add("Familie 2 Personen".into(), FAMILY_TWO); } } else { fee.add_person(self); if self.has_role(db, "paid").await { fee.paid(); } fee.merge(self.fee_without_families(db).await); } Some(fee) } async fn fee_without_families(&self, db: &SqlitePool) -> Fee { let mut fee = Fee::new(); if !self.has_role(db, "Donau Linz").await { return fee; } if self.has_role(db, "Rennrudern").await { fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG); } let amount_boats = self.amount_boats(db).await; if amount_boats > 0 { fee.add( format!("{}x Bootsplatz", amount_boats), amount_boats * BOAT_STORAGE, ); } let halfprice = if let Some(member_since_date) = &self.member_since_date { if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") { let halfprice_startdate = NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap(); member_since_date >= halfprice_startdate } else { false } } else { false }; if self.has_role(db, "Unterstützend").await { fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); } else if self.has_role(db, "Förderndes Mitglied").await { fee.add("Förderndes Mitglied".into(), FOERDERND); } else if Family::find_by_opt_id(db, self.family_id).await.is_none() { if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { if halfprice { fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2); } else { fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL); } } else if self.has_role(db, "Ehrenmitglied").await { fee.add("Ehrenmitglied".into(), 0); } else { if halfprice { fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); } else { fee.add("Mitgliedsbeitrag".into(), REGULAR); } } } fee } pub async fn amount_boats(&self, db: &SqlitePool) -> i32 { sqlx::query!( "SELECT COUNT(*) as count FROM boat WHERE owner = ?", self.id ) .fetch_one(db) .await .unwrap() .count } pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 { sqlx::query!( "SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL", self.id ) .fetch_one(db) .await .unwrap() .count } pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool { if sqlx::query!( "SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)", self.id, role ) .fetch_optional(db) .await .unwrap() .is_some() { return true; } false } pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool { match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id) .fetch_one(db) .await .unwrap() { Some(a) if a.is_empty() => false, None => false, _ => true, } } pub async fn roles(&self, db: &SqlitePool) -> Vec { sqlx::query!( "SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;", self.id ) .fetch_all(db) .await .unwrap() .into_iter().map(|r| r.name).collect() } pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool { if sqlx::query!( "SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)", self.id, role ) .fetch_optional(db.deref_mut()) .await .unwrap() .is_some() { return true; } false } pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE id like ? ", id ) .fetch_one(db) .await .ok() } pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE id like ? ", id ) .fetch_one(db.deref_mut()) .await .ok() } pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { let name = name.trim().to_lowercase(); sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE lower(name)=? ", name ) .fetch_one(db) .await .ok() } pub async fn on_water(&self, db: &SqlitePool) -> bool { if sqlx::query!( "SELECT * FROM logbook WHERE shipmaster=? AND arrival is null", self.id ) .fetch_optional(db) .await .unwrap() .is_some() { return true; } if sqlx::query!( "SELECT * FROM logbook JOIN rower ON rower.logbook_id=logbook.id WHERE rower_id=? AND arrival is null", self.id ) .fetch_optional(db) .await .unwrap() .is_some() { return true; } false } pub async fn all(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 ORDER BY last_access DESC " ) .fetch_all(db) .await .unwrap() } pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec { let mut tx = db.begin().await.unwrap(); let ret = Self::all_with_role_tx(&mut tx, role).await; tx.commit().await.unwrap(); ret } pub async fn all_with_role_tx(db: &mut Transaction<'_, Sqlite>, role: &Role) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user u JOIN user_role ur ON u.id = ur.user_id WHERE ur.role_id = ? AND deleted = 0 ORDER BY name; ", role.id ) .fetch_all(db.deref_mut()) .await .unwrap() } pub async fn all_payer_groups(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id IS NOT NULL GROUP BY family_id UNION -- Select users with a null family_id, without grouping SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id IS NULL; " ) .fetch_all(db) .await .unwrap() } pub async fn ergo(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' ORDER BY name " ) .fetch_all(db) .await .unwrap() } pub async fn cox(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 ORDER BY last_access DESC " ) .fetch_all(db) .await .unwrap() } pub async fn create(db: &SqlitePool, name: &str) -> bool { let name = name.trim(); sqlx::query!("INSERT INTO USER(name) VALUES (?)", name) .execute(db) .await .is_ok() } pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) { let mut family_id = data.family_id; if family_id.is_some_and(|x| x == -1) { family_id = Some(Family::insert(db).await) } if !self.has_membership_pdf(db).await { if let Some(membership_pdf) = data.membership_pdf { let mut stream = membership_pdf.open().await.unwrap(); let mut buffer = Vec::new(); stream.read_to_end(&mut buffer).await.unwrap(); sqlx::query!( "UPDATE user SET membership_pdf = ? where id = ?", buffer, self.id ) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } } sqlx::query!( "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?", data.dob, data.weight, data.sex, data.member_since_date, data.birthdate, data.mail, data.nickname, data.notes, data.phone, data.address, family_id, self.id ) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id // handle roles sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id) .execute(db) .await .unwrap(); for role_id in data.roles.into_keys() { self.add_role( db, &Role::find_by_id(db, role_id.parse::().unwrap()) .await .unwrap(), ) .await; } } pub async fn add_role(&self, db: &SqlitePool, role: &Role) { sqlx::query!( "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", self.id, role.id ) .execute(db) .await .unwrap(); } pub async fn remove_role(&self, db: &SqlitePool, role: &Role) { sqlx::query!( "DELETE FROM user_role WHERE user_id = ? and role_id = ?", self.id, role.id ) .execute(db) .await .unwrap(); } pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result { 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; } return Err(LoginError::InvalidAuthenticationCombo); // Username not found }; if user.deleted { Log::create( db, format!("User ({name}) already deleted (tried to login)."), ) .await; return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has //been deleted } if let Some(user_pw) = user.pw.as_ref() { let password_hash = &Self::get_hashed_pw(pw); if password_hash == user_pw { return Ok(user); } Log::create(db, format!("User {name} supplied the wrong PW")).await; Err(LoginError::InvalidAuthenticationCombo) } else { info!("User {name} has no PW set"); Err(LoginError::NoPasswordSet(Box::new(user))) } } pub async fn reset_pw(&self, db: &SqlitePool) { sqlx::query!("UPDATE user SET pw = null where id = ?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn update_pw(&self, db: &SqlitePool, pw: &str) { let pw = Self::get_hashed_pw(pw); sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } fn get_hashed_pw(pw: &str) -> String { let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap(); let argon2 = Argon2::default(); argon2 .hash_password(pw.as_bytes(), &salt) .unwrap() .to_string() } pub async fn logged_in(&self, db: &SqlitePool) { sqlx::query!( "UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?", self.id ) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn delete(&self, db: &SqlitePool) { sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn get_days(&self, db: &SqlitePool) -> Vec { let mut days = Vec::new(); for i in 0..self.amount_days_to_show(db).await { let date = (Local::now() + chrono::Duration::days(i)).date_naive(); if self.has_role(db, "scheckbuch").await { days.push(Day::new_guest(db, date, false).await); } else { days.push(Day::new(db, date, false).await); } } for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await { if self.has_role(db, "scheckbuch").await { let day = Day::new_guest(db, date, true).await; if !day.events.is_empty() { days.push(day); } } else { days.push(Day::new(db, date, true).await); } } days } async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 { if self.has_role(db, "cox").await { let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok, //december //has 31 //days end_of_year .signed_duration_since(Local::now().date_naive()) .num_days() + 1 } else { 6 } } pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option { let rowed_km = Stat::person(db, None, self).await.rowed_km; if rowed_km % 1000 > 970 { return Some(format!( "{} braucht nur mehr {} km bis die {} km voll sind 🤑", self.name, 1000 - rowed_km % 1000, rowed_km + 1000 - (rowed_km % 1000) )); } None } } #[async_trait] impl<'r> FromRequest<'r> for User { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match req.cookies().get_private("loggedin_user") { Some(user_id) => match user_id.value().parse::() { Ok(user_id) => { let db = req.rocket().state::().unwrap(); let Some(user) = User::find_by_id(db, user_id).await else { return Outcome::Error((Status::Forbidden, LoginError::UserNotFound)); }; if user.deleted { return Outcome::Error((Status::Forbidden, LoginError::UserDeleted)); } user.logged_in(db).await; let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2)); req.cookies().add_private(cookie); Outcome::Success(user) } Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)), }, None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), } } } pub struct TechUser { pub(crate) user: User, } impl Deref for TechUser { type Target = User; fn deref(&self) -> &Self::Target { &self.user } } #[async_trait] impl<'r> FromRequest<'r> for TechUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "tech").await { Outcome::Success(TechUser { user }) } else { Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } pub struct CoxUser { pub(crate) user: User, } impl Deref for CoxUser { type Target = User; fn deref(&self) -> &Self::Target { &self.user } } impl CoxUser { pub async fn new(db: &SqlitePool, user: User) -> Option { if user.has_role(db, "cox").await { Some(CoxUser { user }) } else { None } } } #[async_trait] impl<'r> FromRequest<'r> for CoxUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "cox").await { Outcome::Success(CoxUser { user }) } else { Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct AdminUser { pub(crate) user: User, } #[async_trait] impl<'r> FromRequest<'r> for AdminUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "admin").await { Outcome::Success(AdminUser { user }) } else { Outcome::Forward(Status::Forbidden) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct AllowedForPlannedTripsUser(pub(crate) User); #[async_trait] impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await { Outcome::Success(AllowedForPlannedTripsUser(user)) } else { Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } impl From for User { fn from(val: AllowedForPlannedTripsUser) -> Self { val.0 } } #[derive(Debug, Serialize, Deserialize)] pub struct DonauLinzUser(pub(crate) User); impl From for User { fn from(val: DonauLinzUser) -> Self { val.0 } } impl Deref for DonauLinzUser { type Target = User; fn deref(&self) -> &Self::Target { &self.0 } } #[async_trait] impl<'r> FromRequest<'r> for DonauLinzUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "Donau Linz").await && !user.has_role(db, "Unterstützend").await && !user.has_role(db, "Förderndes Mitglied").await { Outcome::Success(DonauLinzUser(user)) } else { Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct SchnupperBetreuerUser(pub(crate) User); impl From for User { fn from(val: SchnupperBetreuerUser) -> Self { val.0 } } impl Deref for SchnupperBetreuerUser { type Target = User; fn deref(&self) -> &Self::Target { &self.0 } } #[async_trait] impl<'r> FromRequest<'r> for SchnupperBetreuerUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "schnupper-betreuer").await { Outcome::Success(SchnupperBetreuerUser(user)) } else { Outcome::Forward(Status::Forbidden) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct VorstandUser(pub(crate) User); impl From for User { fn from(val: VorstandUser) -> Self { val.0 } } impl Deref for VorstandUser { type Target = User; fn deref(&self) -> &Self::Target { &self.0 } } #[async_trait] impl<'r> FromRequest<'r> for VorstandUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "Vorstand").await { Outcome::Success(VorstandUser(user)) } else { Outcome::Forward(Status::Forbidden) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct EventUser(pub(crate) User); impl From for User { fn from(val: EventUser) -> Self { val.0 } } impl Deref for EventUser { type Target = User; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(FromRow, Serialize, Deserialize, Clone, Debug)] pub struct UserWithRolesAndMembershipPdf { #[serde(flatten)] pub user: User, pub membership_pdf: bool, pub roles: Vec, } impl UserWithRolesAndMembershipPdf { pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self { let membership_pdf = user.has_membership_pdf(db).await; Self { roles: user.roles(db).await, user, membership_pdf, } } } #[derive(FromRow, Serialize, Deserialize, Clone, Debug)] pub struct UserWithMembershipPdf { #[serde(flatten)] pub user: User, pub membership_pdf: Option>, } impl UserWithMembershipPdf { pub(crate) async fn from(db: &SqlitePool, user: User) -> Self { let membership_pdf: Option> = sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = $1", user.id) .fetch_optional(db) .await .unwrap() .unwrap(); Self { user, membership_pdf, } } } #[async_trait] impl<'r> FromRequest<'r> for EventUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { if user.has_role(db, "manage_events").await { Outcome::Success(EventUser(user)) } else { Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[cfg(test)] mod test { use std::collections::HashMap; use crate::{tera::admin::user::UserEditForm, testdb}; use super::User; use sqlx::SqlitePool; #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.id, 1); } #[sqlx::test] fn test_find_wrong_id() { let pool = testdb!(); let user = User::find_by_id(&pool, 1337).await; assert!(user.is_none()); } #[sqlx::test] fn test_find_correct_name() { let pool = testdb!(); let user = User::find_by_name(&pool, "admin".into()).await.unwrap(); assert_eq!(user.id, 1); } #[sqlx::test] fn test_find_wrong_name() { let pool = testdb!(); let user = User::find_by_name(&pool, "name-does-not-exist".into()).await; assert!(user.is_none()); } #[sqlx::test] fn test_all() { let pool = testdb!(); let res = User::all(&pool).await; assert!(res.len() > 3); } #[sqlx::test] fn test_cox() { let pool = testdb!(); let res = User::cox(&pool).await; assert_eq!(res.len(), 3); } #[sqlx::test] fn test_succ_create() { let pool = testdb!(); assert_eq!(User::create(&pool, "new-user-name".into()).await, true); } #[sqlx::test] fn test_duplicate_name_create() { let pool = testdb!(); assert_eq!(User::create(&pool, "admin".into()).await, false); } #[sqlx::test] fn test_update() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); user.update( &pool, UserEditForm { id: 1, dob: None, weight: None, sex: Some("m".into()), roles: HashMap::new(), member_since_date: None, birthdate: None, mail: None, nickname: None, notes: None, phone: None, address: None, family_id: None, membership_pdf: None, }, ) .await; let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.sex, Some("m".into())); } #[sqlx::test] fn succ_login_with_test_db() { let pool = testdb!(); User::login(&pool, "admin".into(), "admin".into()) .await .unwrap(); } #[sqlx::test] fn wrong_pw() { let pool = testdb!(); 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()); } #[sqlx::test] fn reset() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); user.reset_pw(&pool).await; let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.pw, None); } #[sqlx::test] fn update_pw() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); assert!(User::login(&pool, "admin".into(), "abc".into()) .await .is_err()); user.update_pw(&pool, "abc".into()).await; User::login(&pool, "admin".into(), "abc".into()) .await .unwrap(); } }