diff --git a/frontend/scss/components/_headlines.scss b/frontend/scss/components/_headlines.scss index cd4fa44..8bc5a0b 100644 --- a/frontend/scss/components/_headlines.scss +++ b/frontend/scss/components/_headlines.scss @@ -4,4 +4,8 @@ .h2 { @apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3; +} + +.h3 { + @apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white; } \ No newline at end of file diff --git a/src/model/activity.rs b/src/model/activity.rs index a6676d6..c75b813 100644 --- a/src/model/activity.rs +++ b/src/model/activity.rs @@ -1,6 +1,6 @@ use std::ops::DerefMut; -use super::user::User; +use super::{role::Role, user::User}; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; @@ -21,6 +21,7 @@ pub struct ActivityBuilder { } impl ActivityBuilder { + #[must_use] pub fn new(text: &str) -> Self { Self { text: text.into(), @@ -29,6 +30,7 @@ impl ActivityBuilder { } } + #[must_use] pub fn relevant_for_user(self, user: &User) -> Self { Self { relevant_for: format!("{}user-{};", self.relevant_for, user.id), @@ -36,6 +38,14 @@ impl ActivityBuilder { } } + #[must_use] + pub fn relevant_for_role(self, role: &Role) -> Self { + Self { + relevant_for: format!("{}role-{};", self.relevant_for, role.id), + ..self + } + } + pub async fn save(self, db: &SqlitePool) { Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await; } diff --git a/src/model/role.rs b/src/model/role.rs index 07e09a4..6617cc8 100644 --- a/src/model/role.rs +++ b/src/model/role.rs @@ -1,5 +1,6 @@ use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; +use super::{activity::ActivityBuilder, user::AdminUser}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; @@ -134,6 +135,30 @@ WHERE name like ? .ok() } + pub async fn update( + &self, + db: &SqlitePool, + updated_by: &AdminUser, + formatted_name: &str, + desc: &str, + ) -> Result<(), String> { + sqlx::query!( + "UPDATE role SET formatted_name=?, desc=? WHERE id=?", + formatted_name, + desc, + self.id + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + + ActivityBuilder::new(&format!( + "{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert." + )).relevant_for_role(self).save(db).await; + + Ok(()) + } + pub async fn names_from_role(&self, db: &SqlitePool) -> Vec { let query = format!( "SELECT u.name diff --git a/src/model/user/foerdernd.rs b/src/model/user/foerdernd.rs index c51cc7f..6019654 100644 --- a/src/model/user/foerdernd.rs +++ b/src/model/user/foerdernd.rs @@ -1,7 +1,8 @@ -use super::{regular::ClubMember, ManageUserUser, User}; +use super::{ManageUserUser, User, regular::ClubMember}; use crate::{ + NonEmptyString, model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, - special_user, NonEmptyString, + special_user, }; use chrono::NaiveDate; use rocket::{async_trait, fs::TempFile}; diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 0539e08..0e71ca8 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -1,20 +1,21 @@ use std::{fmt::Display, ops::DerefMut}; -use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use argon2::{Argon2, PasswordHasher, password_hash::SaltString}; use chrono::{Datelike, Local, NaiveDate}; use log::info; use rocket::async_trait; use rocket::{ + Request, 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::{ + Day, log::Log, logbook::Logbook, mail::Mail, @@ -23,7 +24,6 @@ use super::{ role::Role, stat::Stat, tripdetails::TripDetails, - Day, }; use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use scheckbuch::ScheckbuchUser; @@ -512,7 +512,7 @@ ASKÖ Ruderverein Donau Linz", self.name), .save(db) .await; return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has - //been deleted + //been deleted } if let Some(user_pw) = user.pw.as_ref() { @@ -583,12 +583,12 @@ ASKÖ Ruderverein Donau Linz", self.name), .await; } - pub async fn delete(&self, db: &SqlitePool) { + pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) { 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.")) + ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht.")) .relevant_for_user(self) .save(db) .await; @@ -622,9 +622,9 @@ ASKÖ Ruderverein Donau Linz", self.name), 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 + //december + //has 31 + //days let days_left_in_year = end_of_year .signed_duration_since(Local::now().date_naive()) .num_days() @@ -633,9 +633,9 @@ ASKÖ Ruderverein Donau Linz", self.name), 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 + //december + //has 31 + //days end_of_next_year .signed_duration_since(Local::now().date_naive()) .num_days() @@ -867,8 +867,8 @@ 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 +// remove -> +// RegularUser special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); special_user!(VorstandUser, +"admin", +"Vorstand"); special_user!(EventUser, +"manage_events"); @@ -982,17 +982,21 @@ mod test { #[sqlx::test] fn wrong_pw() { let pool = testdb!(); - assert!(User::login(&pool, "admin".into(), "admi".into()) - .await - .is_err()); + 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()); + assert!( + User::login(&pool, "admi".into(), "admin".into()) + .await + .is_err() + ); } #[sqlx::test] @@ -1011,9 +1015,11 @@ mod test { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); - assert!(User::login(&pool, "admin".into(), "abc".into()) - .await - .is_err()); + assert!( + User::login(&pool, "admin".into(), "abc".into()) + .await + .is_err() + ); user.update_pw(&pool, "abc".into()).await; diff --git a/src/model/user/regular.rs b/src/model/user/regular.rs index a49b377..438a8a6 100644 --- a/src/model/user/regular.rs +++ b/src/model/user/regular.rs @@ -1,7 +1,8 @@ use super::{ManageUserUser, User}; use crate::{ + NonEmptyString, model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, - special_user, NonEmptyString, + special_user, }; use chrono::NaiveDate; use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt}; diff --git a/src/model/user/scheckbuch.rs b/src/model/user/scheckbuch.rs index 4a58f04..694433b 100644 --- a/src/model/user/scheckbuch.rs +++ b/src/model/user/scheckbuch.rs @@ -2,12 +2,13 @@ use super::foerdernd::FoerderndUser; use super::regular::RegularUser; use super::unterstuetzend::UnterstuetzendUser; use super::{ManageUserUser, User}; +use crate::NonEmptyString; use crate::model::activity::ActivityBuilder; use crate::model::role::Role; -use crate::NonEmptyString; use crate::{ + SCHECKBUCH, model::{mail::Mail, notification::Notification}, - special_user, SCHECKBUCH, + special_user, }; use chrono::NaiveDate; use rocket::async_trait; diff --git a/src/model/user/schnupperant.rs b/src/model/user/schnupperant.rs index 5e00bed..b5addb5 100644 --- a/src/model/user/schnupperant.rs +++ b/src/model/user/schnupperant.rs @@ -4,9 +4,9 @@ use super::scheckbuch::ScheckbuchUser; use super::schnupperinterest::SchnupperInterestUser; use super::unterstuetzend::UnterstuetzendUser; use super::{ManageUserUser, User}; +use crate::NonEmptyString; use crate::model::activity::ActivityBuilder; use crate::model::role::Role; -use crate::NonEmptyString; use crate::{ model::{mail::Mail, notification::Notification}, special_user, diff --git a/src/model/user/schnupperinterest.rs b/src/model/user/schnupperinterest.rs index b6aed52..d046048 100644 --- a/src/model/user/schnupperinterest.rs +++ b/src/model/user/schnupperinterest.rs @@ -1,9 +1,9 @@ use super::scheckbuch::ScheckbuchUser; use super::schnupperant::SchnupperantUser; use super::{ManageUserUser, User}; +use crate::NonEmptyString; use crate::model::activity::ActivityBuilder; use crate::model::role::Role; -use crate::NonEmptyString; use crate::{model::notification::Notification, special_user}; use rocket::async_trait; use sqlx::SqlitePool; diff --git a/src/model/user/unterstuetzend.rs b/src/model/user/unterstuetzend.rs index 716b207..7399406 100644 --- a/src/model/user/unterstuetzend.rs +++ b/src/model/user/unterstuetzend.rs @@ -1,7 +1,8 @@ -use super::{regular::ClubMember, ManageUserUser, User}; +use super::{ManageUserUser, User, regular::ClubMember}; use crate::{ + NonEmptyString, model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, - special_user, NonEmptyString, + special_user, }; use chrono::NaiveDate; use rocket::{async_trait, fs::TempFile}; diff --git a/src/tera/admin/mod.rs b/src/tera/admin/mod.rs index 2c68b4a..53653ff 100644 --- a/src/tera/admin/mod.rs +++ b/src/tera/admin/mod.rs @@ -12,6 +12,7 @@ pub mod boat; pub mod event; pub mod mail; pub mod notification; +pub mod role; pub mod schnupper; pub mod user; @@ -81,6 +82,7 @@ pub fn routes() -> Vec { ret.append(&mut notification::routes()); ret.append(&mut mail::routes()); ret.append(&mut event::routes()); + ret.append(&mut role::routes()); ret.append(&mut routes![rss, show_rss, show_list, list]); ret } diff --git a/src/tera/admin/role.rs b/src/tera/admin/role.rs new file mode 100644 index 0000000..0e051f8 --- /dev/null +++ b/src/tera/admin/role.rs @@ -0,0 +1,64 @@ +use crate::model::{ + role::Role, + user::{AdminUser, UserWithDetails, VorstandUser}, +}; +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +#[get("/role")] +async fn index( + db: &State, + admin: VorstandUser, + flash: Option>, +) -> Template { + let roles = Role::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("roles", &roles); + context.insert( + "loggedin_user", + &UserWithDetails::from_user(admin.user, db).await, + ); + + Template::render("admin/role", context.into_json()) +} +#[derive(FromForm)] +pub struct RoleToUpdate<'r> { + pub formatted_name: &'r str, + pub desc: &'r str, +} + +#[post("/role/", data = "")] +async fn update( + db: &State, + data: Form>, + role_id: i32, + admin: AdminUser, +) -> Flash { + let role = Role::find_by_id(db, role_id).await; + let Some(role) = role else { + return Flash::error(Redirect::to("/admin/role"), "Role does not exist!"); + }; + + match role + .update(db, &admin, &data.formatted_name, &data.desc) + .await + { + Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"), + Err(e) => Flash::error(Redirect::to("/admin/role"), e), + } +} + +pub fn routes() -> Vec { + routes![index, update] +} diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 7978cf9..69ec070 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -7,11 +7,11 @@ use crate::{ mail::valid_mails, role::Role, user::{ + AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, + UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member, regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser, - AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, - UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, }, }, tera::Config, @@ -19,6 +19,7 @@ use crate::{ use chrono::NaiveDate; use futures::future::join_all; use rocket::{ + FromForm, Request, Route, State, form::Form, fs::TempFile, get, @@ -26,9 +27,9 @@ use rocket::{ post, request::{FlashMessage, FromRequest, Outcome}, response::{Flash, Redirect}, - routes, FromForm, Request, Route, State, + routes, }; -use rocket_dyn_templates::{tera::Context, Template}; +use rocket_dyn_templates::{Template, tera::Context}; use sqlx::SqlitePool; // Custom request guard to extract the Referer header @@ -286,7 +287,7 @@ async fn delete(db: &State, admin: ManageUserUser, user: i32) -> Fla Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await; match user { Some(user) => { - user.delete(db).await; + user.delete(db, &admin).await; Flash::success( Redirect::to("/admin/user"), format!("Benutzer {} gelöscht", user.name), diff --git a/templates/admin/role.html.tera b/templates/admin/role.html.tera new file mode 100644 index 0000000..b014c21 --- /dev/null +++ b/templates/admin/role.html.tera @@ -0,0 +1,37 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/boat" as boat %} +{% extends "base" %} +{% block content %} +
+

Rolle

+
+ +
+
+{% endblock content %} diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 2006d9b..d889e2f 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -8,52 +8,121 @@ Neue Person hinzufügen -
- Vereinsmitglied -
-
- - + +
+ + + + + +
+ +
+ +
+

Neues Vereinsmitglied

+ +
+ + +
+ {{ macros::input(label='Name', name='name', type="text", required=true) }} + {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} + {{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} + {{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }} + {{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }} + {{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }} + {{ macros::input(label='Adresse', name='address', type="text", required=true) }} + {{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }} + +
- {{ macros::input(label='Name', name='name', type="text", required=true) }} - {{ macros::input(label='Mailadresse', name='mail', type="email", required=true) }} - {{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} - {{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }} - {{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }} - {{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }} - {{ macros::input(label='Adresse', name='address', type="text", required=true) }} - {{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }} - - -
-
- Scheckbuch -
- {{ macros::input(label='Name', name='name', type="text", required=true) }} - {{ macros::input(label='Mailadresse', name='mail', type="email", required=true) }} - -
-
-
- Schnupperkurs -
+ + + +
+ +
+

Neues Scheckbuch

+ + {{ macros::input(label='Name', name='name', type="text", required=true) }} + {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} + + +
+
+
+ + +
+ +
+
+

Neuer Schnupperant

+
{{ macros::input(label='Name', name='name', type="text", required=true) }} - {{ macros::input(label='Mailadresse', name='mail', type="email", required=true) }} + {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} {{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }}
-
+ + + {% endif %} diff --git a/templates/admin/user/view.html.tera b/templates/admin/user/view.html.tera index 5a515eb..e6a61bb 100644 --- a/templates/admin/user/view.html.tera +++ b/templates/admin/user/view.html.tera @@ -132,7 +132,7 @@