be able to update data individually; Fixes #951

This commit is contained in:
2025-04-30 11:06:10 +02:00
parent 90087843ad
commit c8d5c633d7
8 changed files with 589 additions and 221 deletions

View File

@ -3,7 +3,7 @@ use std::{error::Error, fs};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
Address, Message, SmtpTransport, Transport,
};
use sqlx::{Sqlite, SqlitePool, Transaction};
@ -374,3 +374,13 @@ Der Vorstand");
}
}
}
pub(crate) fn valid_mails(mails: &str) -> bool {
let splitted = mails.split(',');
for single_rec in splitted {
if single_rec.parse::<Address>().is_err() {
return false;
}
}
true
}

View File

@ -1,4 +1,4 @@
use std::ops::DerefMut;
use std::{fmt::Display, ops::DerefMut};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@ -10,6 +10,12 @@ pub struct Role {
pub(crate) cluster: Option<String>,
}
impl Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Role {
pub async fn all(db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(Role, "SELECT id, name, cluster FROM role")

199
src/model/user/basic.rs Normal file
View File

@ -0,0 +1,199 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{log::Log, mail::valid_mails, role::Role};
use sqlx::SqlitePool;
impl User {
pub(crate) async fn update_mail(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_mail: &str,
) -> Result<(), String> {
let new_mail = new_mail.trim();
if !valid_mails(new_mail) {
return Err(format!(
"{new_mail} ist kein gültiges Format für eine Mailadresse"
));
}
sqlx::query!("UPDATE user SET mail = ? where id = ?", new_mail, self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.mail {
Some(old_mail) => format!(
"{updated_by} has changed the mail address of {self} from {old_mail} to {new_mail}"
),
None => format!("{updated_by} has added a mail address for {self}: {new_mail}"),
};
Log::create(db, msg).await;
Ok(())
}
pub(crate) async fn update_phone(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_phone: &str,
) -> Result<(), String> {
let new_phone = new_phone.trim();
let query = if new_phone.is_empty() {
sqlx::query!("UPDATE user SET phone = NULL where id = ?", self.id)
} else {
sqlx::query!("UPDATE user SET phone = ? where id = ?", new_phone, self.id)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.phone {
Some(old_phone) if new_phone.is_empty() => format!("{updated_by} has removed the phone number of {self} (old number: {old_phone})"),
Some(old_phone) => format!("{updated_by} has changed the phone number of {self} from {old_phone} to {new_phone}"),
None => format!("{updated_by} has added a phone number for {self}: {new_phone}")
};
Log::create(db, msg).await;
Ok(())
}
pub(crate) async fn update_nickname(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_nickname: &str,
) -> Result<(), String> {
let new_nickname = new_nickname.trim();
let query = if new_nickname.is_empty() {
sqlx::query!("UPDATE user SET nickname = NULL where id = ?", self.id)
} else {
sqlx::query!(
"UPDATE user SET nickname = ? where id = ?",
new_nickname,
self.id
)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.nickname {
Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {old_nickname})"),
Some(old_nickname) => format!("{updated_by} has changed the nickname of {self} from {old_nickname} to {new_nickname}"),
None => format!("{updated_by} has added a nickname for {self}: {new_nickname}")
};
Log::create(db, msg).await;
Ok(())
}
pub(crate) async fn remove_role(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
role: &Role,
) -> Result<(), String> {
if !self.has_role(db, &role.name).await {
return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"));
}
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
Log::create(
db,
format!("{updated_by} has removed role {role} from user {self}"),
)
.await;
Ok(())
}
pub(crate) async fn has_not_paid(
&self,
db: &SqlitePool,
updated_by: &AllowedToEditPaymentStatusUser,
) {
let paid = Role::find_by_name(db, "paid").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
paid.id
)
.execute(db)
.await
.unwrap();
Log::create(
db,
format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"),
)
.await;
}
pub(crate) async fn has_paid(
&self,
db: &SqlitePool,
updated_by: &AllowedToEditPaymentStatusUser,
) {
let paid = Role::find_by_name(db, "paid").await.unwrap();
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
paid.id
)
.execute(db)
.await
.expect("paid role has no group");
Log::create(
db,
format!("{updated_by} has set that user {self} has paid the fee (yet)"),
)
.await;
}
pub(crate) async fn add_role(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
role: &Role,
) -> Result<(), String> {
if self.has_role(db, &role.name).await {
return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"));
}
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Log::create(
db,
format!("{updated_by} has added role {role} to user {self}"),
)
.await;
Ok(())
}
}

View File

@ -1,4 +1,7 @@
use std::ops::{Deref, DerefMut};
use std::{
fmt::Display,
ops::{Deref, DerefMut},
};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use chrono::{Datelike, Local, NaiveDate};
@ -29,6 +32,7 @@ use super::{
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
use scheckbuch::ScheckbuchUser;
mod basic;
mod fee;
mod scheckbuch;
@ -53,6 +57,12 @@ pub struct User {
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)]
@ -585,26 +595,6 @@ ORDER BY last_access DESC
Ok(())
}
pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Ok(())
}
async fn send_end_mail_scheckbuch(
&self,
db: &mut Transaction<'_, Sqlite>,
@ -658,17 +648,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
Ok(())
}
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<Self, LoginError> {
let name = name.trim().to_lowercase(); // just to make sure...
let Some(user) = User::find_by_name(db, &name).await else {
@ -1000,6 +979,12 @@ macro_rules! special_user {
}
}
}
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)*) => {
{

View File

@ -1,7 +1,7 @@
use super::User;
use crate::model::user::LoginError;
use crate::{
model::{mail::Mail, notification::Notification, role::Role},
model::{mail::Mail, notification::Notification},
special_user, SCHECKBUCH,
};
use rocket::async_trait;
@ -16,24 +16,24 @@ use std::ops::Deref;
special_user!(ScheckbuchUser, +"scheckbuch");
impl ScheckbuchUser {
async fn from(user: User, db: &SqlitePool, mail: &str, smtp_pw: &str) -> Result<(), String> {
// TODO: see when/how to invoke this function (explicit `Neue Person hinzufügen` button?
// Button to transition existing users to scheckbuch? Automatically called when
// `scheckbuch` is newly selected as role?
if user.has_role(db, "scheckbuch").await {
return Err("User is already a scheckbuch".into());
}
//async fn from(user: User, db: &SqlitePool, mail: &str, smtp_pw: &str) -> Result<(), String> {
// // TODO: see when/how to invoke this function (explicit `Neue Person hinzufügen` button?
// // Button to transition existing users to scheckbuch? Automatically called when
// // `scheckbuch` is newly selected as role?
// if user.has_role(db, "scheckbuch").await {
// return Err("User is already a scheckbuch".into());
// }
// TODO: do we allow e.g. DonauLinz to scheckbuch?
// // TODO: do we allow e.g. DonauLinz to scheckbuch?
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
user.add_role(db, &scheckbuch).await.unwrap();
// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
// user.add_role(db, &scheckbuch).await.unwrap();
// TODO: remove all other `membership_type` roles
let new_user = Self::new(db, &user).await.unwrap();
// // TODO: remove all other `membership_type` roles
// let new_user = Self::new(db, &user).await.unwrap();
new_user.notify(db, mail, smtp_pw).await
}
// new_user.notify(db, mail, smtp_pw).await
//}
pub(crate) async fn notify(
&self,