Compare commits

..

2 Commits

Author SHA1 Message Date
91bf71cf00 start working on frontend
Some checks failed
CI/CD Pipeline / test (push) Failing after 9m22s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-11 09:58:53 +02:00
f8f18def90 create cluster in userrole 2024-10-11 09:29:26 +02:00
7 changed files with 208 additions and 33 deletions

View File

@ -27,7 +27,8 @@ CREATE TABLE IF NOT EXISTS "family" (
CREATE TABLE IF NOT EXISTS "role" ( CREATE TABLE IF NOT EXISTS "role" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE "name" text NOT NULL UNIQUE,
"cluster" text
); );
CREATE TABLE IF NOT EXISTS "user_role" ( CREATE TABLE IF NOT EXISTS "user_role" (
@ -220,3 +221,16 @@ CREATE TABLE IF NOT EXISTS "distance" (
"distance_in_km" integer NOT NULL "distance_in_km" integer NOT NULL
); );
CREATE UNIQUE INDEX one_role_per_group_per_user ON user_role
WHERE EXISTS (
SELECT 1
FROM role r1
JOIN role r2 ON r1.id = user_role.role_id
WHERE r1."group" = r2."group"
AND r2.id IN (
SELECT role_id
FROM user_role ur2
WHERE ur2.user_id = user_role.user_id
)
);

View File

@ -1,5 +1,7 @@
use std::ops::DerefMut;
use serde::Serialize; use serde::Serialize;
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool}; use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
use super::user::User; use super::user::User;
@ -22,6 +24,15 @@ impl Family {
.unwrap() .unwrap()
} }
pub async fn insert_tx(db: &mut Transaction<'_, Sqlite>) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db.deref_mut())
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn insert(db: &SqlitePool) -> i64 { pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES") let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db) .execute(db)

View File

@ -1,17 +1,18 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use serde::Serialize; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone, Deserialize, Debug)]
pub struct Role { pub struct Role {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,
pub(crate) cluster: Option<String>,
} }
impl Role { impl Role {
pub async fn all(db: &SqlitePool) -> Vec<Role> { pub async fn all(db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(Role, "SELECT id, name FROM role") sqlx::query_as!(Role, "SELECT id, name, cluster FROM role")
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap() .unwrap()
@ -21,7 +22,7 @@ impl Role {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE id like ? WHERE id like ?
", ",
@ -31,12 +32,41 @@ WHERE id like ?
.await .await
.ok() .ok()
} }
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, cluster
FROM role
WHERE id like ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, cluster
FROM role
WHERE cluster = ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",
@ -51,7 +81,7 @@ WHERE name like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",

View File

@ -490,6 +490,17 @@ ASKÖ Ruderverein Donau Linz", self.name),
_ => true, _ => 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> { pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
sqlx::query!( sqlx::query!(
@ -502,6 +513,21 @@ ASKÖ Ruderverein Donau Linz", self.name),
.into_iter().map(|r| r.name).collect() .into_iter().map(|r| r.name).collect()
} }
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(
Role,
"SELECT r.id, r.name, r.cluster
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 { pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
if sqlx::query!( if sqlx::query!(
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)", "SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
@ -697,14 +723,16 @@ ORDER BY last_access DESC
.is_ok() .is_ok()
} }
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) { pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
let mut db = db.begin().await.map_err(|e| e.to_string())?;
let mut family_id = data.family_id; let mut family_id = data.family_id;
if family_id.is_some_and(|x| x == -1) { if family_id.is_some_and(|x| x == -1) {
family_id = Some(Family::insert(db).await) family_id = Some(Family::insert_tx(&mut db).await)
} }
if !self.has_membership_pdf(db).await { if !self.has_membership_pdf_tx(&mut db).await {
if let Some(membership_pdf) = data.membership_pdf { if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap(); let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
@ -714,7 +742,7 @@ ORDER BY last_access DESC
buffer, buffer,
self.id self.id
) )
.execute(db) .execute(db.deref_mut())
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
} }
@ -735,28 +763,29 @@ ORDER BY last_access DESC
family_id, family_id,
self.id self.id
) )
.execute(db) .execute(db.deref_mut())
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
// handle roles // handle roles
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id) sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
.execute(db) .execute(db.deref_mut())
.await .await
.unwrap(); .unwrap();
for role_id in data.roles.into_keys() { for role_id in data.roles.into_keys() {
self.add_role( let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
db,
&Role::find_by_id(db, role_id.parse::<i32>().unwrap())
.await .await
.unwrap(), .unwrap();
) self.add_role_tx(&mut db, &role).await?;
.await;
}
} }
pub async fn add_role(&self, db: &SqlitePool, role: &Role) { db.commit().await.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> {
sqlx::query!( sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id, self.id,
@ -764,7 +793,40 @@ ORDER BY last_access DESC
) )
.execute(db) .execute(db)
.await .await
.unwrap(); .map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Ok(())
}
pub async fn add_role_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
role: &Role,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db.deref_mut())
.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(())
} }
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) { pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
@ -1107,7 +1169,7 @@ pub struct UserWithRolesAndMembershipPdf {
#[serde(flatten)] #[serde(flatten)]
pub user: User, pub user: User,
pub membership_pdf: bool, pub membership_pdf: bool,
pub roles: Vec<String>, pub roles: Vec<String>, // TODO: get rid of this...
} }
impl UserWithRolesAndMembershipPdf { impl UserWithRolesAndMembershipPdf {

View File

@ -200,7 +200,8 @@ async fn fees_paid(
) )
.await; .await;
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap()) user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await; .await
.expect("paid role has no group");
} }
} }
@ -292,6 +293,8 @@ async fn update(
data: Form<UserEditForm<'_>>, data: Form<UserEditForm<'_>>,
admin: ManageUserUser, admin: ManageUserUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
println!("{data:#?}");
let user = User::find_by_id(db, data.id).await; let user = User::find_by_id(db, data.id).await;
Log::create( Log::create(
db, db,
@ -305,9 +308,10 @@ async fn update(
); );
}; };
user.update(db, data.into_inner()).await; match user.update(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"),
Flash::success(Redirect::to("/admin/user"), "Successfully updated user") Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
} }
#[get("/user/<user>/membership")] #[get("/user/<user>/membership")]
@ -394,7 +398,9 @@ async fn create_scheckbuch(
// 4. Add 'scheckbuch' role // 4. Add 'scheckbuch' role
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
user.add_role(db, &scheckbuch).await; user.add_role(db, &scheckbuch)
.await
.expect("new user has no roles yet");
// 4. Send welcome mail (+ notification) // 4. Send welcome mail (+ notification)
user.send_welcome_email(db, &config.smtp_pw).await.unwrap(); user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
@ -434,10 +440,14 @@ async fn schnupper_to_scheckbuch(
user.remove_role(db, &paid).await; user.remove_role(db, &paid).await;
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
user.add_role(db, &scheckbuch).await; 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 { if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
user.add_role(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(); user.send_welcome_email(db, &config.smtp_pw).await.unwrap();

View File

@ -3,3 +3,35 @@ INSERT INTO user(name) VALUES('Marie');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
INSERT INTO user(name) VALUES('Philipp'); INSERT INTO user(name) VALUES('Philipp');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
ALTER TABLE "role" ADD COLUMN "cluster" text;
CREATE TRIGGER prevent_multiple_roles_same_cluster
BEFORE INSERT ON user_role
BEGIN
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM user_role ur
JOIN role r1 ON ur.role_id = r1.id
JOIN role r2 ON r1."cluster" = r2."cluster"
WHERE ur.user_id = NEW.user_id
AND r2.id = NEW.role_id
AND r1.id != NEW.role_id
)
THEN RAISE(ABORT, 'User already has a role in this cluster')
END;
END;
UPDATE role SET 'cluster'='skill' WHERE id=2;
UPDATE role SET 'cluster'='membership_type' WHERE id=3;
UPDATE role SET 'cluster'='skill' WHERE id=5;
UPDATE role SET 'cluster'='skill' WHERE id=6;
UPDATE role SET 'cluster'='membership_type' WHERE id=7;
UPDATE role SET 'cluster'='financial' WHERE id=8;
UPDATE role SET 'cluster'='membership_type' WHERE id=9;
UPDATE role SET 'cluster'='membership_type' WHERE id=14;
UPDATE role SET 'cluster'='financial' WHERE id=17;
UPDATE role SET 'cluster'='financial' WHERE id=18;
UPDATE role SET 'cluster'='membership_type' WHERE id=20;
UPDATE role SET 'cluster'='membership_type' WHERE id=22;

View File

@ -79,8 +79,24 @@
<div class="w-full grid gap-3 mt-3"> <div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" /> <input type="hidden" name="id" value="{{ user.id }}" />
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for cluster, r in roles | group_by(attribute="cluster") %}
{{ cluster }}
<label for="{{ cluster }}" class="flex items-center cursor-pointer text-black dark:text-white hover:text-gray-900 dark:hover:text-gray-100">
<select id="{{ cluster }}" name="{{ cluster }}" class="h-8 accent-primary-600 dark:accent-primary-200 mr-2">
<option value="">None</option>
{% for role in r %}
<option value="{{ role.id }}">{{ role.name }}</option>
{% endfor %}
</select>
</label>
{% endfor %}
{% for role in roles %} {% for role in roles %}
{% if not role.cluster %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }} {{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{% endif %}
{% endfor %} {% endfor %}
<hr class="sm:col-span-2 lg:col-span-4 my-3" /> <hr class="sm:col-span-2 lg:col-span-4 my-3" />
{% if user.membership_pdf %} {% if user.membership_pdf %}