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, 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; #[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(User), 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> { if !self.has_role(db, "Donau Linz").await { return Err(format!( "Could not send welcome mail, because user {} is not in Donau Linz group", self.name )); } let Some(mail) = &self.mail else { return Err(format!( "Could not send welcome mail, because user {} has no email address", self.name )); }; 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 und it@rudernlinz.at 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. 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; Log::create( db, format!("Willkommensemail wurde an {} versandt", self.name), ) .await; 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, ) .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, ); } 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 { fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL); } else if self.has_role(db, "Ehrenmitglied").await { fee.add("Ehrenmitglied".into(), 0); } 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 { 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 name like ? ", 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(); // just to make sure... let Some(user) = User::find_by_name(db, name).await else { if ![ "n-sageder", "p-hofer", "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", "d.kortschak", "[login]", ] .contains(&name) { 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(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.planned_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 } } } #[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 PlannedEventUser(pub(crate) User); impl From for User { fn from(val: PlannedEventUser) -> Self { val.0 } } impl Deref for PlannedEventUser { 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 PlannedEventUser { 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, "planned_event").await { Outcome::Success(PlannedEventUser(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(); } }