Merge branch 'single-user-edit-page' of https://git.hofer.link/Ruderverein-Donau-Linz/rowt into single-user-edit-page
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped

This commit is contained in:
Marie Birner 2025-05-05 20:21:11 +02:00
commit b7094bff06
14 changed files with 187 additions and 35 deletions

View File

@ -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;
}

View File

@ -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<String> {
let query = format!(
"SELECT u.name

View File

@ -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};

View File

@ -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() {
@ -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;

View File

@ -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};

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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};

View File

@ -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<Route> {
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
}

64
src/tera/admin/role.rs Normal file
View File

@ -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<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> 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/<role_id>", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<RoleToUpdate<'_>>,
role_id: i32,
admin: AdminUser,
) -> Flash<Redirect> {
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<Route> {
routes![index, update]
}

View File

@ -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

View File

@ -0,0 +1,37 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/boat" as boat %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Rolle</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Rolle</h2>
{% for role in roles %}
<div data-filterable="true"
data-filter="{{ role.name }}"
class="w-full border-t">
<form action="/admin/role/{{ role.id }}"
data-filterable="true"
method="post"
class="bg-white dark:bg-primary-900 p-4 w-full">
<div class="w-full">
<input type="hidden" name="id" value="{{ role.id }}" />
<div class="font-bold mb-1 text-black dark:text-white">
{{ role.name }}
<br />
</div>
<div class="grid md:grid-cols-3 gap-3">
{{ macros::input(label='Formatierter Name', name='formatted_name', type='text', value=role.formatted_name) }}
{{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }}
<input value="Ändern" type="submit" class="w-28 btn btn-primary" />
</div>
</div>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -431,6 +431,9 @@
<li class="py-1">
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
</li>
<li class="py-1">
<a href="/admin/role" class="block w-100 py-2 hover:text-primary-600">Rollen</a>
</li>
<li class="py-1">
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
</li>