rowt/src/tera/admin/user.rs
Philipp Hofer d2914f9287
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
be able to update data individually; Fixes #952
2025-04-30 13:38:45 +02:00

837 lines
24 KiB
Rust

use std::collections::HashMap;
use crate::{
model::{
family::Family,
log::Log,
logbook::Logbook,
role::Role,
user::{
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
},
},
tera::Config,
};
use chrono::NaiveDate;
use futures::future::join_all;
use rocket::{
form::Form,
fs::TempFile,
get,
http::{ContentType, Status},
post,
request::{FlashMessage, FromRequest, Outcome},
response::{Flash, Redirect},
routes, FromForm, Request, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
// Custom request guard to extract the Referer header
struct Referer(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Referer {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get_one("Referer") {
Some(referer) => Outcome::Success(Referer(referer.to_string())),
None => Outcome::Error((Status::BadRequest, ())),
}
}
}
#[get("/user?<sort>&<asc>")]
async fn index(
db: &State<SqlitePool>,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
sort: Option<String>,
asc: bool,
) -> Template {
let sort_column = sort.unwrap_or_else(|| "last_access".to_string());
let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc)
.await
.into_iter()
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
.collect();
let user: User = user.into_inner();
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json())
}
#[get("/user", rank = 2)]
async fn index_admin(
db: &State<SqlitePool>,
user: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user_futures: Vec<_> = User::all(db)
.await
.into_iter()
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
.collect();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let user: User = user.user;
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json())
}
#[get("/user/<user>")]
async fn view(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
user: i32,
) -> Result<Template, Flash<Redirect>> {
let Some(user) = User::find_by_id(db, user).await else {
return Err(Flash::error(
Redirect::to("/admin/usert"),
format!("User mit ID {} gibts ned", user),
));
};
let user = UserWithRolesAndMembershipPdf::from_user(db, user).await;
let admin: User = admin.into_inner();
let allowed_to_edit = ManageUserUser::new(db, &admin).await.is_some();
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("user", &user);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin, db).await,
);
Ok(Template::render("admin/user/view", context.into_json()))
}
#[get("/user/fees")]
async fn fees(
db: &State<SqlitePool>,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let users = User::all_payer_groups(db).await;
let mut fees = Vec::new();
for user in users {
if let Some(fee) = user.fee(db).await {
fees.push(fee);
}
}
context.insert("fees", &fees);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("admin/user/fees", context.into_json())
}
#[get("/user/scheckbuch")]
async fn scheckbuch(
db: &State<SqlitePool>,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let scheckbooks = Role::find_by_name(db, "scheckbuch").await.unwrap();
let scheckbooks = User::all_with_role(db, &scheckbooks).await;
let mut scheckbooks_with_roles = Vec::new();
for s in scheckbooks {
scheckbooks_with_roles.push((
Logbook::completed_with_user(db, &s).await,
UserWithDetails::from_user(s, db).await,
))
}
context.insert("scheckbooks", &scheckbooks_with_roles);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("admin/user/scheckbuch", context.into_json())
}
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
calling_user: AllowedToEditPaymentStatusUser,
user_ids: Vec<i32>,
referer: Referer,
) -> Flash<Redirect> {
let mut res = String::new();
for user_id in user_ids {
let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await {
user.has_not_paid(db, &calling_user).await;
} else {
user.has_paid(db, &calling_user).await;
}
}
res.truncate(res.len() - 3); // remove ' + ' from the end
Flash::success(
Redirect::to(referer.0),
format!("Zahlungsstatus von {} erfolgreich geändert", res),
)
}
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: ManageUserUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
match user {
Some(user) => {
Log::create(
db,
format!("{} has resetted the pw for {}", admin.user.name, user.name),
)
.await;
user.reset_pw(db).await;
Flash::success(
Redirect::to("/admin/user"),
format!("Passwort von {} zurückgesetzt", user.name),
)
}
None => Flash::error(Redirect::to("/admin/user"), "User does not exist"),
}
}
#[get("/user/<user>/delete")]
async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user {
Some(user) => {
user.delete(db).await;
Flash::success(
Redirect::to("/admin/user"),
format!("Benutzer {} gelöscht", user.name),
)
}
None => Flash::error(Redirect::to("/admin/user"), "User does not exist"),
}
}
#[derive(FromForm, Debug)]
pub struct UserEditForm<'a> {
pub(crate) id: i32,
pub(crate) dob: Option<String>,
pub(crate) weight: Option<String>,
pub(crate) sex: Option<String>,
pub(crate) roles: HashMap<String, String>,
pub(crate) member_since_date: Option<String>,
pub(crate) birthdate: Option<String>,
pub(crate) mail: Option<String>,
pub(crate) nickname: Option<String>,
pub(crate) notes: Option<String>,
pub(crate) phone: Option<String>,
pub(crate) address: Option<String>,
pub(crate) family_id: Option<i64>,
pub(crate) membership_pdf: Option<TempFile<'a>>,
}
#[post("/user", data = "<data>", format = "multipart/form-data")]
async fn update(
db: &State<SqlitePool>,
data: Form<UserEditForm<'_>>,
admin: ManageUserUser,
) -> Flash<Redirect> {
let user = User::find_by_id(db, data.id).await;
Log::create(
db,
format!("{} updated user from {user:?} to {data:?}", admin.user.name),
)
.await;
let Some(user) = user else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", data.id),
);
};
match user.update(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct MailUpdateForm {
mail: String,
}
#[post("/user/<id>/change-mail", data = "<data>")]
async fn update_mail(
db: &State<SqlitePool>,
data: Form<MailUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.update_mail(db, &admin, &data.mail).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Mailadresse erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct PhoneUpdateForm {
phone: String,
}
#[post("/user/<id>/change-phone", data = "<data>")]
async fn update_phone(
db: &State<SqlitePool>,
data: Form<PhoneUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.update_phone(db, &admin, &data.phone).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Telefonnummer erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddressUpdateForm {
address: String,
}
#[post("/user/<id>/change-address", data = "<data>")]
async fn update_address(
db: &State<SqlitePool>,
data: Form<AddressUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.update_address(db, &admin, &data.address).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Adresse erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct FamilyUpdateForm {
family_id: Option<i64>,
}
#[post("/user/<id>/change-family", data = "<data>")]
async fn update_family(
db: &State<SqlitePool>,
data: Form<FamilyUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let family = match data.family_id {
Some(-1) => Some(
Family::find_by_id(db, Family::insert(db).await)
.await
.unwrap(),
),
Some(id) => match Family::find_by_id(db, id).await {
Some(f) => Some(f),
None => {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!("Family with ID {} does not exist!", id),
);
}
},
None => None,
};
user.update_family(db, &admin, family).await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Familie erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct AddMembershipPDFForm<'a> {
membership_pdf: TempFile<'a>,
}
#[post("/user/<id>/add-membership-pdf", data = "<data>")]
async fn add_membership_pdf(
db: &State<SqlitePool>,
data: Form<AddMembershipPDFForm<'_>>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user
.add_membership_pdf(db, &admin, &data.membership_pdf)
.await
{
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Beitrittserklärung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct NicknameUpdateForm {
nickname: String,
}
#[post("/user/<id>/change-nickname", data = "<data>")]
async fn update_nickname(
db: &State<SqlitePool>,
data: Form<NicknameUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.update_nickname(db, &admin, &data.nickname).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Spitzname erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct MemberSinceUpdateForm {
member_since: String,
}
#[post("/user/<id>/change-member-since", data = "<data>")]
async fn update_member_since(
db: &State<SqlitePool>,
data: Form<MemberSinceUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Ok(new_member_since_date) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d")
else {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!(
"Datum {} ist nicht im YYYY-MM-DD Format",
&data.member_since
),
);
};
user.update_member_since(db, &admin, &new_member_since_date)
.await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Beitrittsdatum erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct BirthdateUpdateForm {
birthdate: String,
}
#[post("/user/<id>/change-birthdate", data = "<data>")]
async fn update_birthdate(
db: &State<SqlitePool>,
data: Form<BirthdateUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Ok(new_birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate),
);
};
user.update_birthdate(db, &admin, &new_birthdate).await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Geburtstag erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct AddRoleForm {
role_id: i32,
}
#[post("/user/<id>/add-role", data = "<data>")]
async fn add_role(
db: &State<SqlitePool>,
data: Form<AddRoleForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(role) = Role::find_by_id(db, data.role_id).await else {
return Flash::error(
Redirect::to("/admin/user/{user_id}"),
format!("Role with ID {} does not exist!", data.role_id),
);
};
match user.add_role(db, &admin, &role).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Rolle erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[get("/user/<user_id>/remove-role/<role_id>")]
async fn remove_role(
db: &State<SqlitePool>,
admin: ManageUserUser,
user_id: i32,
role_id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user_id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", user_id),
);
};
let Some(role) = Role::find_by_id(db, role_id).await else {
return Flash::error(
Redirect::to("/admin/user/{user_id}"),
format!("Role with ID {} does not exist!", role_id),
);
};
match user.remove_role(db, &admin, &role).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Rolle erfolgreich gelöscht",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[get("/user/<user>/membership")]
async fn download_membership_pdf(
db: &State<SqlitePool>,
admin: ManageUserUser,
user: i32,
) -> (ContentType, Vec<u8>) {
let user = User::find_by_id(db, user).await.unwrap();
let user = UserWithMembershipPdf::from(db, user).await;
Log::create(
db,
format!(
"{} downloaded membership application for user: {}",
admin.user.name, user.user.name
),
)
.await;
(ContentType::PDF, user.membership_pdf.unwrap())
}
#[derive(FromForm, Debug)]
struct UserAddForm<'r> {
name: &'r str,
}
#[post("/user/new", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>,
admin: ManageUserUser,
) -> Flash<Redirect> {
if User::create(db, data.name).await {
Log::create(
db,
format!("{} created new user: {data:?}", admin.user.name),
)
.await;
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
} else {
Flash::error(
Redirect::to("/admin/user"),
format!("User {} already exists", data.name),
)
}
}
#[derive(FromForm, Debug)]
struct UserAddScheckbuchForm<'r> {
name: &'r str,
mail: &'r str,
}
//#[post("/user/new/scheckbuch", data = "<data>")]
//async fn create_scheckbuch(
// db: &State<SqlitePool>,
// data: Form<UserAddScheckbuchForm<'_>>,
// admin: VorstandUser,
// config: &State<Config>,
//) -> Flash<Redirect> {
// // 1. Check mail adress
// let mail = data.mail.trim();
// if mail.parse::<Address>().is_err() {
// return Flash::error(
// Redirect::to("/admin/user/scheckbuch"),
// "Keine gültige Mailadresse".to_string(),
// );
// }
//
// // 2. Check name
// let name = data.name.trim();
// if User::find_by_name(db, name).await.is_some() {
// return Flash::error(
// Redirect::to("/admin/user/scheckbuch"),
// "Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
// .to_string(),
// );
// }
//
// // 3. Create user
// User::create_with_mail(db, name, mail).await;
// let user = User::find_by_name(db, name).await.unwrap();
//
// // 4. Add 'scheckbuch' role
// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
// user.add_role(db, &scheckbuch)
// .await
// .expect("new user has no roles yet");
//
// // 4. Send welcome mail (+ notification)
// user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
//
// Log::create(
// db,
// format!("{} created new scheckbuch: {data:?}", admin.name),
// )
// .await;
// Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
//}
//#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
//async fn schnupper_to_scheckbuch(
// db: &State<SqlitePool>,
// id: i32,
// admin: SchnupperBetreuerUser,
// config: &State<Config>,
//) -> Flash<Redirect> {
// let Some(user) = User::find_by_id(db, id).await else {
// return Flash::error(
// Redirect::to("/admin/schnupper"),
// "user id not found".to_string(),
// );
// };
//
// if !user.has_role(db, "schnupperant").await {
// return Flash::error(
// Redirect::to("/admin/schnupper"),
// "kein schnupperant...".to_string(),
// );
// }
//
// let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
// let paid = Role::find_by_name(db, "paid").await.unwrap();
// user.remove_role(db, &schnupperant).await;
// user.remove_role(db, &paid).await;
//
// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
// user.add_role(db, &scheckbuch)
// .await
// .expect("just removed 'schnupperant' thus can't have a role with that group");
//
// if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
// user.add_role(db, &no_einschreibgebuehr)
// .await
// .expect("role doesn't have a group");
// }
//
// user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
//
// Log::create(
// db,
// format!(
// "{} created new scheckbuch (from schnupperant): {}",
// admin.name, user.name
// ),
// )
// .await;
// Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
//}
pub fn routes() -> Vec<Route> {
routes![
index,
index_admin,
view,
resetpw,
update,
create,
//create_scheckbuch,
//schnupper_to_scheckbuch,
delete,
fees,
fees_paid,
scheckbuch,
download_membership_pdf,
send_welcome_mail,
//
update_mail,
update_phone,
update_nickname,
update_member_since,
update_birthdate,
update_address,
update_family,
add_membership_pdf,
add_role,
remove_role,
]
}