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.
This commit is contained in:
89
src/main.rs
89
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<Template> {
|
||||
let uid = user.uid;
|
||||
let (tids, tnames) = table::get_tids(&conn, uid).await;
|
||||
let mut cols = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
for tblid in tids.clone() {
|
||||
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await.unwrap();
|
||||
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await;
|
||||
if tbl.is_none() {
|
||||
return None;
|
||||
}
|
||||
let tbl = tbl.unwrap();
|
||||
cols.push(tbl.column_names.clone());
|
||||
let mut rws : Vec<Vec<String>> = Vec::new();
|
||||
for row in tbl.rows {
|
||||
@@ -61,15 +69,51 @@ async fn home(conn: Db, user: AuthUser) -> Template {
|
||||
}
|
||||
rows.push(rws.clone());
|
||||
}
|
||||
Template::render("home",
|
||||
context!{
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
}
|
||||
let mut shares = conn.run(move |c| inventur_db::get_shared_tbls(c, user.uid)).await;
|
||||
if shares.is_none() {
|
||||
shares = Some(Vec::new());
|
||||
}
|
||||
let shares = shares.unwrap();
|
||||
let mut sharetblids = Vec::new();
|
||||
for tbl in shares {
|
||||
sharetblids.push(tbl.tblid);
|
||||
}
|
||||
let sharetblids = sharetblids;
|
||||
let mut sharetbls = Vec::new();
|
||||
for tblid in sharetblids.clone() {
|
||||
let sharetbl = conn.run(move |c| inventur_db::get_table_shared(c, tblid, uid)).await;
|
||||
if sharetbl.is_some() {
|
||||
sharetbls.push(sharetbl.unwrap());
|
||||
}
|
||||
}
|
||||
let sharetbls = sharetbls;
|
||||
let mut sharedcols = Vec::new();
|
||||
let mut sharedrows = Vec::new();
|
||||
let mut sharednms = Vec::new();
|
||||
for sharetbl in sharetbls {
|
||||
sharedcols.push(sharetbl.column_names.clone());
|
||||
let mut rws: Vec<Vec<String>> = Vec::new();
|
||||
for row in sharetbl.rows {
|
||||
rws.push(row.cells);
|
||||
}
|
||||
sharednms.push(sharetbl.name);
|
||||
sharedrows.push(rws.clone());
|
||||
}
|
||||
Some(
|
||||
Template::render("home",
|
||||
context!{
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
sharedtblids: sharetblids,
|
||||
sharednms: sharednms,
|
||||
sharedcols: sharedcols,
|
||||
sharedrows: sharedrows,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,7 +137,21 @@ async fn favicon() -> Redirect {
|
||||
Redirect::to(uri!("/img/favicon.ico"))
|
||||
}
|
||||
|
||||
/// Setup app for launch:
|
||||
#[post("/users", data="<data>")]
|
||||
async fn search_users(conn: Db, data: Form<SearchString>, user: AuthUser) -> Result<Json<Vec<UserQueryResult>>, Status> {
|
||||
let result = conn.run(move |c| inventur_db::query_users(c, data.query.clone(), user.uid)).await;
|
||||
if result.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
let result = result.unwrap();
|
||||
let mut queryresults: Vec<UserQueryResult> = Vec::new();
|
||||
for u in result {
|
||||
queryresults.push( UserQueryResult { id: u.uid, username: u.uname } );
|
||||
}
|
||||
Ok(rocket::serde::json::Json(queryresults))
|
||||
}
|
||||
|
||||
/// Set app up for launch:
|
||||
/// Load configuration from a file called .env in the project's root.
|
||||
/// Use tera templates, connect to mysql db, setup oauth
|
||||
/// Serve everything related to ...
|
||||
@@ -114,10 +172,11 @@ async fn rocket() -> _ {
|
||||
.attach(Template::fairing())
|
||||
.attach(Db::fairing())
|
||||
.attach(OAuth2::<auth::RanderathIdentity>::fairing("oauth"))
|
||||
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout])
|
||||
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table])
|
||||
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout, search_users])
|
||||
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table, table::edit_share])
|
||||
.mount("/row", routes![table::new_entry, table::edit_entry, table::delete_entry])
|
||||
.mount("/column", routes![table::delete_column, table::edit_column, table::create_column])
|
||||
.mount("/table/readonly", routes![table::table_readonly])
|
||||
.register("/", catchers![auth::redirect_to_login_401, auth::redirect_to_login_403])
|
||||
.mount("/img", FileServer::from(relative!("static/img")))
|
||||
.mount("/css", FileServer::from(relative!("static/css")))
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
|
||||
//! Submodule holding structs relevant to handle form data.
|
||||
|
||||
use rocket::serde::Deserialize;
|
||||
use rocket::serde::{Serialize, Deserialize};
|
||||
//use inventur_db::ShareCard;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct DeleteColumn {
|
||||
@@ -87,4 +88,38 @@ pub struct DeleteTable {
|
||||
pub tblid: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Sharee {
|
||||
id: i32,
|
||||
username: String,
|
||||
readonly: bool,
|
||||
}
|
||||
|
||||
impl From<inventur_db::ShareCard> for Sharee {
|
||||
fn from(sharecard: inventur_db::ShareCard) -> Self {
|
||||
Sharee { id: sharecard.sharee_id, username: sharecard.sharee_name, readonly: sharecard.readonly}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct EditShare {
|
||||
pub sharees: Vec<i32>,
|
||||
pub readonly: Vec<bool>,
|
||||
pub delete: Vec<bool>,
|
||||
pub tblid: i32,
|
||||
pub new_user: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct UserQueryResult {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct SearchString {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use rocket::serde::json::Json;
|
||||
|
||||
use inventur_db::FIELDTYPE;
|
||||
|
||||
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname};
|
||||
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname, EditShare};
|
||||
use self::table_view::rocket_uri_macro_table_sec;
|
||||
|
||||
/// Delete a table.
|
||||
@@ -88,4 +88,34 @@ pub async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Resu
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
/// Edit or create the sharees of a table
|
||||
#[post("/share", data="<data>")]
|
||||
pub async fn edit_share(conn: Db, data: Form<EditShare>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = data.tblid;
|
||||
let ro = data.readonly.clone();
|
||||
let mut sharees = data.sharees.clone();
|
||||
if data.new_user.is_some() {
|
||||
sharees.push(data.new_user.unwrap());
|
||||
}
|
||||
let sharees = sharees;
|
||||
for (i, sharee) in sharees.iter().enumerate() {
|
||||
let sharee = sharee.clone();
|
||||
let reado = ro[i].clone();
|
||||
if conn.run(move |c| inventur_db::share(c, tblid, &sharee, &reado, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
let del = data.delete.clone();
|
||||
for (i, todel) in del.iter().enumerate() {
|
||||
if *todel {
|
||||
let sharee = sharees[i].clone();
|
||||
if conn.run(move |c| inventur_db::unshare(c, tblid, &sharee, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,29 +20,36 @@
|
||||
|
||||
use crate::auth;
|
||||
use crate::Db;
|
||||
use crate::table::forms;
|
||||
use inventur_db;
|
||||
|
||||
use auth::AuthUser;
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket::response::Redirect;
|
||||
use forms::Sharee;
|
||||
|
||||
/// View an authenticated user sees visiting the /table/<table id> page
|
||||
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
|
||||
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let mut table = table.unwrap();
|
||||
use inventur_db::Tbl;
|
||||
|
||||
struct PassTbl {
|
||||
tbl: Tbl,
|
||||
searchvalue: String,
|
||||
searchfields: Vec<i32>,
|
||||
sortdir: u8,
|
||||
sortfield: usize,
|
||||
tname: String,
|
||||
column_names: Vec<String>,
|
||||
column_types: Vec<i32>,
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
async fn sort_and_search(tbl: Tbl, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>) -> PassTbl {
|
||||
let searchvalue;
|
||||
let searchfields;
|
||||
let mut tbl = tbl;
|
||||
if search_value.is_some() && !search_value.clone().unwrap().eq_ignore_ascii_case("") && search_fields.is_some() {
|
||||
searchvalue = search_value.unwrap();
|
||||
searchfields = search_fields.unwrap();
|
||||
table = inventur_db::search_table(table, searchfields.clone(), searchvalue.clone());
|
||||
tbl = inventur_db::search_table(tbl, searchfields.clone(), searchvalue.clone());
|
||||
}else {
|
||||
if search_value.is_none() {
|
||||
searchvalue = String::new();
|
||||
@@ -50,7 +57,7 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
|
||||
searchvalue = search_value.unwrap();
|
||||
}
|
||||
if search_fields.is_none() {
|
||||
searchfields = (0i32..(table.column_names.len() as i32)).collect();
|
||||
searchfields = (0i32..(tbl.column_names.len() as i32)).collect();
|
||||
|
||||
} else {
|
||||
searchfields = search_fields.unwrap();
|
||||
@@ -69,33 +76,97 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
|
||||
}else {
|
||||
sortfield = sort_field.unwrap();
|
||||
}
|
||||
let table = inventur_db::sort_table(table, sortfield, sortdir);
|
||||
|
||||
let column_names = tbl.column_names.clone();
|
||||
let column_types = tbl.column_types.clone().iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
|
||||
let rows : Vec<Vec<String>>= tbl.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
|
||||
|
||||
PassTbl { tbl: inventur_db::sort_table(tbl.clone(), sortfield, sortdir), searchvalue, searchfields, sortdir, sortfield, tname: tbl.name, column_names, column_types, rows }
|
||||
|
||||
}
|
||||
|
||||
/// View an authenticated user sees visiting the /table/<table id> page
|
||||
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
|
||||
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
|
||||
let table = passtable.tbl;
|
||||
let tname = table.name;
|
||||
let column_names = table.column_names;
|
||||
let column_types = table.column_types.iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
|
||||
let rows : Vec<Vec<String>>= table.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
|
||||
|
||||
let (tids, tnames) = get_tids(&conn, uid).await;
|
||||
|
||||
let sharecards = conn.run(move |c| inventur_db::get_sharees(c, tid, uid)).await;
|
||||
let mut sharees : Vec<Sharee> = Vec::new();
|
||||
if sharecards.is_some() {
|
||||
sharees = sharecards.unwrap().iter().map(|s| Sharee::from((*s).clone())).collect();
|
||||
}
|
||||
let sharees = sharees;
|
||||
|
||||
Some(
|
||||
Template::render("table",
|
||||
Template::render("table_owned",
|
||||
context!{
|
||||
search_value: searchvalue,
|
||||
search_fields: searchfields,
|
||||
sort_field: sortfield,
|
||||
sort_dir: sortdir,
|
||||
base_url: "/table",
|
||||
search_value: passtable.searchvalue,
|
||||
search_fields: passtable.searchfields,
|
||||
sort_field: passtable.sortfield,
|
||||
sort_dir: passtable.sortdir,
|
||||
tblid: tid,
|
||||
tblname: tname,
|
||||
tblname: passtable.tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
column_names: column_names,
|
||||
column_types: column_types,
|
||||
rows: rows,
|
||||
column_names: passtable.column_names,
|
||||
column_types: passtable.column_types,
|
||||
rows: passtable.rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
shared: sharees,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table_readonly(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table_shared(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
|
||||
let table = passtable.tbl;
|
||||
|
||||
let (tids, tnames) = get_tids(&conn, uid).await;
|
||||
|
||||
Some(
|
||||
Template::render("table_readonly",
|
||||
context!{
|
||||
base_url: "/table/readonly",
|
||||
search_value: passtable.searchvalue,
|
||||
search_fields: passtable.searchfields,
|
||||
sort_field: passtable.sortfield,
|
||||
sort_dir: passtable.sortdir,
|
||||
tblid: tid,
|
||||
tblname: passtable.tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
column_names: passtable.column_names,
|
||||
column_types: passtable.column_types,
|
||||
rows: passtable.rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/// View to redirect a post request handled to manipulate a table or its display representation back to the (new) table view.
|
||||
|
||||
Reference in New Issue
Block a user