rowt/src/model/user/mod.rs
Philipp Hofer e853381bd7
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Fill acitivites from various activities; Fixes #972
2025-05-04 18:38:14 +02:00

1025 lines
34 KiB
Rust

use std::{fmt::Display, ops::DerefMut};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use chrono::{Datelike, Local, NaiveDate};
use log::info;
use rocket::async_trait;
use rocket::{
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::{
log::Log,
logbook::Logbook,
mail::Mail,
notification::Notification,
personal::{equatorprice, rowingbadge},
role::Role,
stat::Stat,
tripdetails::TripDetails,
Day,
};
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use scheckbuch::ScheckbuchUser;
mod basic;
pub(crate) mod clubmember;
mod fee;
pub(crate) mod foerdernd;
pub(crate) mod member;
pub(crate) mod regular;
pub(crate) mod scheckbuch;
pub(crate) mod schnupperant;
pub(crate) mod schnupperinterest;
pub(crate) mod unterstuetzend;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
pub id: i64,
pub name: String,
pub pw: Option<String>,
pub dob: Option<String>,
pub weight: Option<String>,
pub sex: Option<String>,
pub deleted: bool,
pub last_access: Option<chrono::NaiveDateTime>,
pub member_since_date: Option<String>,
pub birthdate: Option<String>,
pub mail: Option<String>,
pub nickname: Option<String>,
pub notes: Option<String>,
pub phone: Option<String>,
pub address: Option<String>,
pub family_id: Option<i64>,
pub user_token: String,
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithDetails {
#[serde(flatten)]
pub user: User,
pub amount_unread_notifications: i64,
pub allowed_to_steer: bool,
pub on_water: bool,
pub roles: Vec<String>,
}
impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
let allowed_to_steer = user.allowed_to_steer(db).await;
Self {
on_water: user.on_water(db).await,
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
allowed_to_steer,
user,
}
}
}
#[derive(Debug)]
pub enum LoginError {
InvalidAuthenticationCombo,
UserNotFound,
UserDeleted,
NotLoggedIn,
NotAnAdmin,
NotACox,
NotATech,
GuestNotAllowed,
NoPasswordSet(Box<User>),
DeserializationError,
}
impl User {
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
}
pub async fn allowed_to_steer_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
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) -> i64 {
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 allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self)
.await
.is_some()
}
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 has_membership_pdf_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
.fetch_one(db.deref_mut())
.await
.unwrap()
{
Some(a) if a.is_empty() => false,
None => false,
_ => true,
}
}
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
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 financial(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'financial';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn skill(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'skill';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(
Role,
"SELECT r.id, r.name, r.cluster, r.formatted_name, r.desc, r.hide_in_lists
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()
}
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<Self> {
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, user_token
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<Self> {
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, user_token
FROM user
WHERE id like ?
",
id
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
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, user_token
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<Self> {
Self::all_with_order(db, "last_access", false).await
}
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
let mut query = format!(
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE deleted = 0
ORDER BY {}
",
sort
);
if !asc {
query.push_str(" DESC");
}
sqlx::query_as::<_, User>(&query)
.fetch_all(db)
.await
.unwrap()
}
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
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<Self> {
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, user_token
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<Self> {
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, user_token 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, user_token FROM user
WHERE family_id IS NULL;
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
Self::all_with_role(db, &ergo).await
}
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
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, user_token
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 update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
dob,
weight,
sex,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
async fn send_end_mail_scheckbuch(
&self,
db: &mut Transaction<'_, Sqlite>,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
Mail::send_single_tx(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
format!(
"Hallo {0},
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).relevant_for_user(&self).save_tx(db).await;
Ok(())
}
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;
}
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
};
if user.deleted {
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde."
))
.relevant_for_user(&user)
.save(db)
.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);
}
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db)
.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
// TODO: add responsible person
ActivityBuilder::new(&format!("Passwort von User {self} wurde zurückgesetzt."))
.relevant_for_user(&self)
.save(db)
.await;
}
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
ActivityBuilder::new(&format!(
"Passwort von User {self} wurde erfolgreich geändert."
))
.relevant_for_user(&self)
.save(db)
.await;
}
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
ActivityBuilder::new(&format!("User {self} hat sich eingeloggt."))
.relevant_for_user(&self)
.save(db)
.await;
}
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
ActivityBuilder::new(&format!("User {self} wurde gelöscht."))
.relevant_for_user(&self)
.save(db)
.await;
}
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
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
}
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
let days_left_in_year = end_of_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1;
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
end_of_next_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1
} else {
days_left_in_year
}
} else {
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
}
}
pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option<String> {
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
}
pub(crate) async fn received_new_logentry(
&self,
db: &mut Transaction<'_, Sqlite>,
smtp_pw: &str,
) {
if self.has_role_tx(db, "scheckbuch").await {
let amount_trips = Logbook::completed_with_user_tx(db, self).await.len();
match amount_trips {
5 => {
if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
}
Notification::create_for_steering_people_tx(
db,
&format!(
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
self.name
),
"Scheckbuch fertig",
None,None
)
.await;
ActivityBuilder::new(&format!("5 Scheckbuchausfahrten von {self} wurden mit der heutigen Ausfahrt aufgebraucht. Info-Mail wurde an {self} geschickt + alle Steuerberechtigten informiert, dass wir pot. ein neues Mitglied haben"))
.relevant_for_user(&self)
.save_tx(db)
.await;
}
a if a > 5 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
self.name, amount_trips
),
"Scheckbuch überfertig",
None,None
)
.await;
ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert."))
.relevant_for_user(&self)
.save_tx(db)
.await;
}
_ => {}
}
}
// check fahrtenabzeichen fertig
if rowingbadge::Status::completed_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für das diesjährige Fahrtenabzeichen erfüllt.",
self.name
),
"Fahrtenabzeichen geschafft",
None,None
)
.await;
ActivityBuilder::new(&format!(
"{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert."
))
.relevant_for_user(&self)
.save_tx(db)
.await;
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
}
// check äquatorpreis geschafft?
if let Some(level) = equatorprice::new_level_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für den Äquatorpreis in {level} geschafft.",
self.name
),
"Äquatorpreis",
None,None
)
.await;
ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert."))
.relevant_for_user(&self)
.save_tx(db)
.await;
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for User {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
match req.cookies().get_private("loggedin_user") {
Some(user_id) => match user_id.value().parse::<i32>() {
Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().unwrap();
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)),
}
}
}
/// Creates a struct named $name. Allows to be created from a user, if one of the specified $roles are active for the user.
#[macro_export]
macro_rules! special_user {
($name:ident, $($role:tt)*) => {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct $name {
pub(crate) user: User,
}
impl std::ops::Deref for $name {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.user
}
}
impl $name {
pub fn into_inner(self) -> User {
self.user
}
}
#[async_trait]
impl<'r> rocket::request::FromRequest<'r> for $name {
type Error = crate::model::user::LoginError;
async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
rocket::request::Outcome::Success(user) => {
if special_user!(@check_roles user, db, $($role)*) {
rocket::request::Outcome::Success($name { user })
} else {
rocket::request::Outcome::Forward(rocket::http::Status::Forbidden)
}
}
rocket::request::Outcome::Error(f) => rocket::request::Outcome::Error(f),
rocket::request::Outcome::Forward(f) => rocket::request::Outcome::Forward(f),
}
}
}
impl $name {
pub async fn new(db: &SqlitePool, user: &User) -> Option<Self> {
if special_user!(@check_roles user, db, $($role)*) {
Some($name { user: user.clone() })
} else {
None
}
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
};
(@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => {
{
let mut has_positive_role = false;
$(
if $user.has_role($db, $role).await {
has_positive_role = true;
}
)*
has_positive_role
$(
&& !$user.has_role($db, $neg_role).await
)*
}
};
}
special_user!(TechUser, +"tech");
special_user!(ErgoUser, +"ergo");
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
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
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)]
pub struct UserWithRolesAndMembershipPdf {
#[serde(flatten)]
pub user: User,
pub membership_pdf: bool,
pub roles: Vec<String>, // TODO: remove
pub proper_roles: Vec<Role>,
}
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,
proper_roles: user.real_roles(db).await,
user,
membership_pdf,
}
}
}
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct UserWithMembershipPdf {
#[serde(flatten)]
pub user: User,
pub membership_pdf: Option<Vec<u8>>,
}
impl UserWithMembershipPdf {
pub(crate) async fn from(db: &SqlitePool, user: User) -> Self {
let membership_pdf: Option<Vec<u8>> =
sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = $1", user.id)
.fetch_optional(db)
.await
.unwrap()
.unwrap();
Self {
user,
membership_pdf,
}
}
}
#[cfg(test)]
mod test {
use crate::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(), 4);
}
#[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();
}
}