From 61708e5199e2c6fe7a0600fa2aa71eae7cca4952 Mon Sep 17 00:00:00 2001 From: Johannes Randerath Date: Sun, 1 Sep 2024 20:14:31 +0200 Subject: [PATCH] Sharing tables readonly with other users - Added sharing feature for table owners to share their tables with other registered users. - Fixed a bug where the wrong entries would be deleted or modified when searching or filtering. --- inventur_db/diesel.toml | 3 +- .../2024-08-31-094645_create_shares/down.sql | 1 + .../2024-08-31-094645_create_shares/up.sql | 13 ++ inventur_db/src/lib.rs | 121 +++++++++++++++++- inventur_db/src/models.rs | 22 +++- inventur_db/src/schema.rs | 29 ++--- inventur_db/src/shares.rs | 53 ++++++++ inventur_db/src/users.rs | 9 +- src/main.rs | 89 ++++++++++--- src/table/forms.rs | 37 +++++- src/table/table_manipulate_table.rs | 32 ++++- src/table/table_view.rs | 117 +++++++++++++---- static/js/table.js | 80 ++++-------- static/js/table_owned.js | 71 ++++++++++ templates/base_modals.html.tera | 4 +- templates/home.html.tera | 46 +++++++ templates/table.html.tera | 53 ++++---- templates/table_owned.html.tera | 25 ++++ templates/table_owned_modals.html.tera | 65 ++++++++++ templates/table_readonly.html.tera | 1 + templates/table_write.html.tera | 38 ++++++ ...html.tera => table_write_modals.html.tera} | 24 ++-- 22 files changed, 766 insertions(+), 167 deletions(-) create mode 100644 inventur_db/migrations/2024-08-31-094645_create_shares/down.sql create mode 100644 inventur_db/migrations/2024-08-31-094645_create_shares/up.sql create mode 100644 inventur_db/src/shares.rs create mode 100644 static/js/table_owned.js create mode 100644 templates/table_owned.html.tera create mode 100644 templates/table_owned_modals.html.tera create mode 100644 templates/table_readonly.html.tera create mode 100644 templates/table_write.html.tera rename templates/{table_modals.html.tera => table_write_modals.html.tera} (83%) diff --git a/inventur_db/diesel.toml b/inventur_db/diesel.toml index df72bdd..5d12181 100644 --- a/inventur_db/diesel.toml +++ b/inventur_db/diesel.toml @@ -24,4 +24,5 @@ file = "src/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] -dir = "/opt/inventur/inventur/inventur_db/migrations" +dir = "/home/johannes/code/inventur/inventur_db/migrations" + diff --git a/inventur_db/migrations/2024-08-31-094645_create_shares/down.sql b/inventur_db/migrations/2024-08-31-094645_create_shares/down.sql new file mode 100644 index 0000000..e636f67 --- /dev/null +++ b/inventur_db/migrations/2024-08-31-094645_create_shares/down.sql @@ -0,0 +1 @@ +DROP TABLE shares; diff --git a/inventur_db/migrations/2024-08-31-094645_create_shares/up.sql b/inventur_db/migrations/2024-08-31-094645_create_shares/up.sql new file mode 100644 index 0000000..e308c42 --- /dev/null +++ b/inventur_db/migrations/2024-08-31-094645_create_shares/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE shares ( + id INTEGER AUTO_INCREMENT UNIQUE NOT NULL, + PRIMARY KEY(id), + sharee_id INTEGER NOT NULL, + FOREIGN KEY (sharee_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + tblid INTEGER NOT NULL, + FOREIGN KEY (tblid) REFERENCES jrtables(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + readonly BOOLEAN NOT NULL DEFAULT true +); diff --git a/inventur_db/src/lib.rs b/inventur_db/src/lib.rs index 5004d26..16299b5 100644 --- a/inventur_db/src/lib.rs +++ b/inventur_db/src/lib.rs @@ -26,6 +26,7 @@ mod jrcolumns; mod jrentries; mod jrcells; mod users; +mod shares; use dotenvy::dotenv; use std::env; @@ -38,11 +39,12 @@ use diesel::deserialize; use diesel::backend::Backend; use diesel::sql_types::Integer; use diesel::expression::Expression; +use models::Share; /// Every column has a type. /// The default is text, currently number (isize) exists too. /// Types are mainly used for sorting and can be helpful in client applications. -#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow)] +#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow, Debug)] #[repr(i32)] pub enum FIELDTYPE { TEXT = 0, @@ -75,6 +77,7 @@ impl From for FIELDTYPE { /// represents and summarised all relevant data of a table. /// Standard return type if whole tables should be returned +#[derive(Clone, Debug)] pub struct Tbl { /// table id of the represented object pub tblid: i32, @@ -109,6 +112,14 @@ pub struct User { pub email: String, } +#[derive(Clone)] +pub struct ShareCard { + pub tblid: i32, + pub sharee_id: i32, + pub sharee_name: String, + pub readonly: bool, +} + /// Connect to database. /// Return value must be passed to most of the crate's functions. pub fn establish_connection() -> MysqlConnection { @@ -130,14 +141,21 @@ pub fn establish_connection() -> MysqlConnection { /// if there has been another error /// pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { + let tbl = fetch_table(conn, tblid); + if tbl.is_none() || + tbl.clone().unwrap().1 != uid + { + return None; + } + Some(tbl.unwrap().0) +} + +fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{ let tbl = jrtables::get_tbl(conn, tblid); if tbl.is_err() { return None; } let tbl = tbl.unwrap(); - if tbl.owner_id != uid { - return None; - } let clmns = jrcolumns::get_clmns_of(conn, tblid); if clmns.is_err() { return None; @@ -172,7 +190,20 @@ pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option Option { + let tbl = fetch_table(conn, tblid); + let share = shares::is_shared(conn, tblid, uid); + if !share { + return None; + } + let tbl = tbl; + if tbl.is_none() { + return None; + } + Some(tbl.unwrap().0) } /// Take a Tbl object, sort it according to parameters and return a sorted Tbl object. @@ -468,3 +499,83 @@ pub fn edit_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_ } Some(true) } + + +pub fn share(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, readonly: &bool, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + println!(":)"); + if owner.is_err() || + owner.unwrap() != uid || + shares::edit_or_create(conn, tblid, sharee_id, readonly).is_err() + { + return None; + } + Some(true) +} + +pub fn unshare(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + shares::delete(conn, tblid, sharee_id).is_err() + { + return None; + } + Some(true) +} + +pub fn get_sharees(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option> { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid + { + return None; + } + let result = shares::get_shares_tbl(conn, tblid); + if result.is_err() { + return None; + } + let result = result.unwrap(); + let mut sharecards = Vec::new(); + for share in result { + let uname = users::get_uname(conn, uid); + if uname.is_err() { + continue; + } + sharecards.push(ShareCard { tblid: tblid, sharee_id: share.sharee_id, sharee_name: uname.unwrap(), readonly: share.readonly }); + } + return Some(sharecards) +} + +pub fn get_shared_tbls(conn: &mut MysqlConnection, uid: i32) -> Option> { + let result = shares::get_shares_usr(conn, uid); + if result.is_err() { + return None; + } + let result = result.unwrap(); + let uname = users::get_uname(conn, uid); + if uname.is_err() { + return None; + } + let uname = uname.unwrap(); + let mut sharecards = Vec::new(); + for share in result { + sharecards.push(ShareCard { tblid: share.tblid, sharee_id: uid, sharee_name: uname.clone(), readonly: share.readonly}); + } + Some(sharecards) +} + +pub fn query_users(conn: &mut MysqlConnection, query: String, uid: i32) -> Option> { + let result = users::query_users(conn, query); + if users::get_uname(conn, uid).is_err() || + result.is_err() + { + return None; + } + let result = result.unwrap(); + let mut pubusers: Vec = Vec::new(); + for user in result { + pubusers.push( User { uid: user.id, uname: user.username, email: user.email } ); + } + Some(pubusers) +} diff --git a/inventur_db/src/models.rs b/inventur_db/src/models.rs index 8928326..cafa6f8 100644 --- a/inventur_db/src/models.rs +++ b/inventur_db/src/models.rs @@ -16,7 +16,7 @@ */ use diesel::prelude::*; -use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users}; +use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users, shares}; #[derive(Queryable, Selectable, Identifiable, Associations)] #[diesel(table_name = crate::schema::jrtables)] @@ -114,3 +114,23 @@ pub struct NewUser { pub email: String, } +#[derive(Insertable)] +#[diesel(table_name = shares)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewShare { + pub sharee_id: i32, + pub readonly: bool, + pub tblid: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = shares)] +#[diesel(belongs_to(Jrtable, foreign_key = tblid))] +#[diesel(belongs_to(User, foreign_key = sharee_id))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Share { + pub id: i32, + pub sharee_id: i32, + pub tblid: i32, + pub readonly: bool, +} diff --git a/inventur_db/src/schema.rs b/inventur_db/src/schema.rs index ce9d7f3..9581276 100644 --- a/inventur_db/src/schema.rs +++ b/inventur_db/src/schema.rs @@ -1,20 +1,3 @@ -/* Simple web app using rocket to help maintain inventory data. - * Copyright (C) 2024 Johannes Randerath - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // @generated automatically by Diesel CLI. diesel::table! { @@ -55,6 +38,15 @@ diesel::table! { } } +diesel::table! { + shares (id) { + id -> Integer, + sharee_id -> Integer, + tblid -> Integer, + readonly -> Bool, + } +} + diesel::table! { users (id) { id -> Integer, @@ -70,11 +62,14 @@ diesel::joinable!(jrcells -> jrentries (jrentry_id)); diesel::joinable!(jrcolumns -> jrtables (jrtable_id)); diesel::joinable!(jrentries -> jrtables (jrtable_id)); diesel::joinable!(jrtables -> users (owner_id)); +diesel::joinable!(shares -> jrtables (tblid)); +diesel::joinable!(shares -> users (sharee_id)); diesel::allow_tables_to_appear_in_same_query!( jrcells, jrcolumns, jrentries, jrtables, + shares, users, ); diff --git a/inventur_db/src/shares.rs b/inventur_db/src/shares.rs new file mode 100644 index 0000000..f3c70d4 --- /dev/null +++ b/inventur_db/src/shares.rs @@ -0,0 +1,53 @@ +use crate::models; +use crate::schema; + +use models::{NewShare, Share}; +use schema::shares::dsl::{id, shares, tblid, sharee_id, readonly}; +use diesel::mysql::MysqlConnection; +use diesel::prelude::*; + + +pub fn edit_or_create(conn: &mut MysqlConnection, tableid: i32, sharee: &i32, ro: &bool) -> Result { + let share = shares.filter(tblid.eq(tableid)) + .filter(sharee_id.eq(sharee)) + .select(id) + .first::(conn); + if share.is_err() { + return diesel::insert_into(schema::shares::table) + .values(&(NewShare { tblid: tableid, sharee_id: *sharee, readonly: *ro })) + .execute(conn); + } + return diesel::update(shares.find(share.unwrap())) + .set(readonly.eq(ro)) + .execute(conn); +} + +pub fn delete(conn: &mut MysqlConnection, tableid: i32, sharee: &i32) -> Result { + diesel::delete(shares.filter(tblid.eq(tableid)).filter(sharee_id.eq(*sharee))).execute(conn) +} + +pub fn get_shares_tbl(conn: &mut MysqlConnection, tableid: i32) -> Result, diesel::result::Error> { + shares + .filter(tblid.eq(tableid)) + .select(Share::as_select()) + .load::(conn) +} + +pub fn get_shares_usr(conn: &mut MysqlConnection, uid: i32) -> Result, diesel::result::Error> { + shares + .filter(sharee_id.eq(uid)) + .select(Share::as_select()) + .load::(conn) +} + +pub fn is_shared(conn: &mut MysqlConnection, tableid: i32, uid: i32) -> bool { + let share = shares + .filter(sharee_id.eq(uid)) + .filter(tblid.eq(tableid)) + .execute(conn); + if share.is_err() { + return false; + } + println!("{share:?}"); + share.unwrap() >= 1 +} diff --git a/inventur_db/src/users.rs b/inventur_db/src/users.rs index 29d6add..87ad5a1 100644 --- a/inventur_db/src/users.rs +++ b/inventur_db/src/users.rs @@ -20,7 +20,7 @@ use crate::models; use crate::schema; use schema::users::dsl::{users, id, username, email}; -use models::NewUser; +use models::{ User, NewUser }; use diesel::prelude::*; use diesel::mysql::MysqlConnection; @@ -58,4 +58,11 @@ pub fn get_uname(conn: &mut MysqlConnection, uid: i32) -> Result(conn) } +pub fn query_users(conn: &mut MysqlConnection, query: String) -> Result, diesel::result::Error> { + users + .filter(username.like(format!("{query}%"))) + .select(User::as_select()) + .load::(conn) +} + diff --git a/src/main.rs b/src/main.rs index 7a21860..f3be99e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,11 @@ use std::env; use dotenvy::dotenv; use rocket::Config; use rocket::figment::providers::Env; -use rocket::http::CookieJar; +use rocket::http::{CookieJar, Status}; +use rocket::form::Form; +use rocket::serde::json::Json; + +use table::forms::{SearchString, UserQueryResult}; /// Database connection using diesel and rocket_sync_db_pools #[database("inventur")] @@ -47,13 +51,17 @@ pub struct Db(diesel::MysqlConnection); /// Home page featuring a preview view of all the user's tables /// Needs an authenticated user. #[get("/")] -async fn home(conn: Db, user: AuthUser) -> Template { +async fn home(conn: Db, user: AuthUser) -> Option