diff --git a/Cargo.lock b/Cargo.lock index f7b9c8d..5e7341a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,16 +677,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -891,12 +881,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "double-ended-peekable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" - [[package]] name = "dsl_auto_type" version = "0.1.0" @@ -1056,22 +1040,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fjall" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8b3cbbdfa775c311965846c523ae291327a5cc3e433479583922ff9527594" -dependencies = [ - "byteorder", - "crc32fast", - "fs_extra", - "log", - "lsm-tree", - "path-absolutize", - "std-semaphore", - "tempfile", -] - [[package]] name = "flate2" version = "1.0.30" @@ -1134,12 +1102,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1295,12 +1257,6 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" -[[package]] -name = "guardian" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6817154789d2e9bb2af0486500e774af579d0e6539247044f06d803b141448b5" - [[package]] name = "hashbrown" version = "0.12.3" @@ -1721,38 +1677,6 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "lsm-tree" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "792f0f9d75b518035f7247774580ba60ee211d381237946e1a40609433f5573f" -dependencies = [ - "byteorder", - "chrono", - "crc32fast", - "crossbeam-skiplist", - "double-ended-peekable", - "fs_extra", - "guardian", - "log", - "lz4_flex", - "path-absolutize", - "quick_cache", - "rand 0.8.5", - "serde", - "serde_json", - "tempfile", -] - -[[package]] -name = "lz4_flex" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" -dependencies = [ - "twox-hash", -] - [[package]] name = "matchers" version = "0.1.0" @@ -1924,14 +1848,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "model_derive" -version = "0.1.0" -dependencies = [ - "quote", - "syn 2.0.66", -] - [[package]] name = "moka" version = "0.12.7" @@ -2188,24 +2104,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "path-absolutize" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" -dependencies = [ - "path-dedot", -] - -[[package]] -name = "path-dedot" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] - [[package]] name = "pathdiff" version = "0.2.1" @@ -2356,11 +2254,9 @@ dependencies = [ "diesel_migrations", "dotenvy", "env_logger", - "fjall", "free-icons", "minijinja", "minijinja-autoreload", - "model_derive", "rand 0.8.5", "redb", "serde", @@ -2465,16 +2361,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "quick_cache" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347e1a588d1de074eeb3c00eadff93db4db65aeb62aee852b1efd0949fe65b6c" -dependencies = [ - "equivalent", - "hashbrown 0.14.5", -] - [[package]] name = "quote" version = "1.0.36" @@ -3282,18 +3168,6 @@ dependencies = [ "serde", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "std-semaphore" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ae9eec00137a8eed469fb4148acd9fc6ac8c3f9b110f52cd34698c8b5bfa0e" - [[package]] name = "stringprep" version = "0.1.5" @@ -3764,16 +3638,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] - [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 6853701..b5b484e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,11 +20,9 @@ diesel = { version = "2.2.0", features = ["extras", "returning_clauses_for_sqlit diesel_migrations = { version = "2.2.0", features = ["sqlite"] } dotenvy = "0.15.7" env_logger = "0.11.3" -fjall = "0.6.5" free-icons = "0.7.0" minijinja = { version = "1.0.14", features = ["loader", "json", "builtins"] } minijinja-autoreload = "1.0.14" -model_derive = { path = "./model_derive" } rand = "0.8.5" redb = "2.1.0" serde = { version = "1.0.197", features = ["derive"] } diff --git a/migrations/2024-05-31-204416_permissions/down.sql b/migrations/2024-05-31-204416_permissions/down.sql index 795cf92..a7f4c83 100644 --- a/migrations/2024-05-31-204416_permissions/down.sql +++ b/migrations/2024-05-31-204416_permissions/down.sql @@ -1,2 +1 @@ DROP TABLE IF EXISTS project_memberships; -DROP TABLE IF EXISTS document_memberships; diff --git a/migrations/2024-05-31-204416_permissions/up.sql b/migrations/2024-05-31-204416_permissions/up.sql index fadfb8c..d3044cd 100644 --- a/migrations/2024-05-31-204416_permissions/up.sql +++ b/migrations/2024-05-31-204416_permissions/up.sql @@ -9,14 +9,3 @@ CREATE TABLE IF NOT EXISTS project_memberships( created TIMESTAMP NOT NULL, updated TIMESTAMP NOT NULL ); - -CREATE TABLE IF NOT EXISTS document_memberships( - id INTEGER PRIMARY KEY NOT NULL UNIQUE, - - user_id UUID_TEXT NOT NULL, - document_id UUID_TEXT NOT NULL, - role TEXT NOT NULL, - - created TIMESTAMP NOT NULL, - updated TIMESTAMP NOT NULL -); diff --git a/model_derive/Cargo.lock b/model_derive/Cargo.lock deleted file mode 100644 index 93b7aef..0000000 --- a/model_derive/Cargo.lock +++ /dev/null @@ -1,46 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "model_derive" -version = "0.1.0" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "syn" -version = "2.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/model_derive/Cargo.toml b/model_derive/Cargo.toml deleted file mode 100644 index 3489eb2..0000000 --- a/model_derive/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "model_derive" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0.36" -syn = { version = "2.0.61", features = ["full", "derive"] } diff --git a/model_derive/src/lib.rs b/model_derive/src/lib.rs deleted file mode 100644 index 996a23d..0000000 --- a/model_derive/src/lib.rs +++ /dev/null @@ -1,48 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{parse_macro_input, DeriveInput, LitInt}; - -#[proc_macro_derive(Model, attributes(model_version))] -pub fn model(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let attrs = input.attrs; - - let model_version = attrs.iter().find_map(|attr| { - if attr.path().is_ident("model_version") { - attr.parse_args::().ok().and_then(|lit| { - Some(lit.base10_parse::().unwrap()) - }) - } else { - None - } - }).unwrap_or(0); - - let lower_name = name.to_string().to_lowercase(); - let lower_name_ident = format_ident!("{}", lower_name); - - - let expanded = quote! { - impl Model for #name { - type Id = uuid::Uuid; - - fn id(&self) -> Self::Id { - self.id - } - - fn key(id: Self::Id) -> Vec { - let mut key = vec![]; - key.extend_from_slice(format!("{}:{}:", #lower_name, #model_version).as_bytes()); - key.extend_from_slice(&id.into_bytes()); - key - } - - fn partition(kv_handle: &KvHandle) -> &PartitionHandle { - &kv_handle.#lower_name_ident - } - } - }; - - TokenStream::from(expanded) -} diff --git a/src/bin/admin.rs b/src/bin/admin.rs index fa075b2..3d6c7c9 100644 --- a/src/bin/admin.rs +++ b/src/bin/admin.rs @@ -60,7 +60,7 @@ pub enum AdminCommand { } async fn handle_create_user(db_url: &str, new_user: NewUser) -> Result<()> { - let mut db = establish_connection(&db_url); + let mut db = establish_connection(db_url); let user = users::Query::new(&mut db).create(new_user)?; println!("User created successfully with id = {}", user.id); @@ -69,7 +69,7 @@ async fn handle_create_user(db_url: &str, new_user: NewUser) -> Result<()> { } async fn handle_list_users(db_url: &str) -> Result<()> { - let mut db = establish_connection(&db_url); + let mut db = establish_connection(db_url); let users = users::Query::new(&mut db).all()?; diff --git a/src/context.rs b/src/context.rs index 6a4e2ca..ce7ca63 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,27 +6,20 @@ use std::sync::Arc; use minijinja_autoreload::AutoReloader; -use crate::{handler::internal_server_error, kv::KvHandle, prelude::*}; +use crate::{handler::internal_server_error, prelude::*}; pub type ConnectionPool = Pool>; #[derive(Clone)] pub struct Context { pub db_pool: ConnectionPool, - // TODO: add a design doc explaining why this not relational - pub kv_handles: KvHandle, template_loader: Arc, } impl Context { - pub fn new( - db: ConnectionPool, - kv_handles: KvHandle, - template_loader: AutoReloader, - ) -> Context { + pub fn new(db: ConnectionPool, template_loader: AutoReloader) -> Context { Context { db_pool: db, - kv_handles, template_loader: Arc::new(template_loader), } } diff --git a/src/db.rs b/src/db.rs index ad71932..ae6ce79 100644 --- a/src/db.rs +++ b/src/db.rs @@ -4,6 +4,8 @@ use diesel::r2d2::Pool; use diesel::SqliteConnection; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use crate::{password, prelude::*}; + pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); /// Establishes a connection to the database using the given URL. @@ -44,10 +46,6 @@ pub fn migrate(conn: &mut SqliteConnection) { conn.run_pending_migrations(MIGRATIONS).unwrap(); } -use thiserror::Error; - -use crate::{password, prelude::*}; - pub struct NewUser { pub full_name: String, pub email: String, diff --git a/src/handler/documents.rs b/src/handler/documents.rs index 1a394aa..5c97cd4 100644 --- a/src/handler/documents.rs +++ b/src/handler/documents.rs @@ -1,48 +1,59 @@ -use axum::{extract::Path, response::Redirect, Form}; +use axum::{extract::Path, http::StatusCode, response::Redirect, Form}; use axum_login::AuthSession; -use crate::{handler::internal_server_error, models::users::User, prelude::*}; +use crate::{ + handler::internal_error, + models::{ + documents::{self, NewDocument}, + users::User, + }, + permissions::{self, query::Permission}, + prelude::*, +}; pub async fn documents_page( State(ctx): State, auth_session: AuthSession, -) -> Response { +) -> Result { if let Some(user) = auth_session.user { render_documents_page(ctx, user).await } else { - Redirect::to("/login").into_response() + Ok(Redirect::to("/login").into_response()) } } -async fn render_documents_page(ctx: Context, user: User) -> Response { - todo!() - //let documents = ModelPermission::user_documents(&ctx.kv_handles, user.id).unwrap_or_default(); +async fn render_documents_page(ctx: Context, user: User) -> Result { + let mut db = ctx.db_pool.get().map_err(internal_error)?; + let documents = + permissions::query::accessible_documents(&mut db, &user.id).map_err(internal_error)?; - //let values = context! { - // user => user, - // documents => documents, - //}; + let values = context! { + user => user, + documents => documents, + }; - //ctx.render_resp("documents/list_documents.html", values) + Ok(ctx.render_resp("documents/list_documents.html", values)) } pub async fn create_document_page( State(ctx): State, auth_session: AuthSession, -) -> Response { - todo!() - //let user = match auth_session.user { - // Some(user) => user, - // None => return Redirect::to("/login").into_response(), - //}; +) -> Result { + let user = match auth_session.user { + Some(user) => user, + None => return Ok(Redirect::to("/login").into_response()), + }; - //let projects = ModelPermission::user_projects(&ctx.kv_handles, user.id).unwrap_or_default(); + let mut db = ctx.db_pool.get().map_err(internal_error)?; - //let values = context! { - // user => user, - // projects => projects, - //}; - //ctx.render_resp("documents/create_document.html", values) + let projects = + permissions::query::accessible_projects(&mut db, &user.id).map_err(internal_error)?; + + let values = context! { + user => user, + projects => projects, + }; + Ok(ctx.render_resp("documents/create_document.html", values)) } #[derive(Debug, Deserialize)] @@ -55,77 +66,69 @@ pub async fn create_document_submit( State(ctx): State, auth_session: AuthSession, form: Form, -) -> Response { - todo!() - //let user = match auth_session.user { - // Some(user) => user, - // None => return Redirect::to("/login").into_response(), - //}; +) -> Result { + let user = match auth_session.user { + Some(user) => user, + None => return Ok(Redirect::to("/login").into_response()), + }; + let mut db = ctx.db_pool.get().map_err(internal_error)?; - //let project = match ModelPermission::user_project(&ctx.kv_handles, user.id, form.project_id) { - // Ok(Some(project)) => project, - // Ok(None) => return Redirect::to("/documents/create").into_response(), - // Err(err) => { - // error!(?err, "failed to access kv store"); - // return Redirect::to("/documents/create").into_response(); - // } - //}; + let project_allowed = permissions::query::check_user_project( + &mut db, + &user.id, + &form.project_id.to_string(), + Permission::Write, + ) + .map_err(internal_error)?; - //let document = Document { - // id: Uuid::now_v7(), - // project_id: project.id, - // title: form.title.to_owned(), - // content: "".to_owned(), - //}; + if !project_allowed { + return Err((StatusCode::FORBIDDEN, "permission denied".to_owned())); + } - //if let Err(err) = document.save(&ctx.kv_handles) { - // error!(?err, "failed to save document"); - // return internal_server_error(); - //} - //info!(?document, "document created"); + let new_document = NewDocument::new( + &user.id, + &form.project_id.to_string(), + form.title.to_owned(), + "".to_owned(), + ); - //let permission = ModelPermission { - // user_id: user.id, - // model_type: ModelType::Document, - // role: Permission::Admin, - // model_id: document.id, - //}; + let document = documents::query::create(&mut db, new_document).map_err(internal_error)?; + info!(?document, "document created"); - //if let Err(err) = permission.add(&ctx.kv_handles) { - // error!(?err, "failed to save new project permission"); - // return internal_server_error(); - //} - - //Redirect::to("/documents").into_response() + Ok(Redirect::to("/documents").into_response()) } pub async fn edit_document_page( State(ctx): State, auth_session: AuthSession, Path((id,)): Path<(Uuid,)>, -) -> Response { - todo!() - //let user = match auth_session.user { - // Some(user) => user, - // None => return Redirect::to("/login").into_response(), - //}; +) -> Result { + let user = match auth_session.user { + Some(user) => user, + None => return Ok(Redirect::to("/login").into_response()), + }; - //let document = match ModelPermission::user_document(&ctx.kv_handles, user.id, id) { - // Ok(Some(document)) => document, - // Ok(None) => return Redirect::to("/documents").into_response(), - // Err(err) => { - // error!(?err, "failed to load document"); - // return internal_server_error(); - // } - //}; + let mut db = ctx.db_pool.get().map_err(internal_error)?; - //dbg!(&document); - //let values = context! { - // user => user, - // document => document, - //}; + let document_allowed = permissions::query::check_user_document( + &mut db, + &user.id, + &id.to_string(), + Permission::Write, + ).map_err(internal_error)?; - //ctx.render_resp("documents/edit_document.html", values) + if !document_allowed { + return Err((StatusCode::FORBIDDEN, "permission denied".to_owned())); + } + + let document = documents::query::by_id(&mut db, &id.to_string()).map_err(internal_error)?; + + let values = context! { + user => user, + document => document, + }; + + Ok(ctx.render_resp("documents/edit_document.html", values)) } #[derive(Debug, Deserialize)] @@ -139,34 +142,31 @@ pub async fn edit_document_submit( auth_session: AuthSession, Path((document_id,)): Path<(Uuid,)>, form: Form, -) -> Response { - todo!() - //let user = match auth_session.user { - // Some(user) => user, - // None => return Redirect::to("/login").into_response(), - //}; +) -> Result { + let user = match auth_session.user { + Some(user) => user, + None => return Ok(Redirect::to("/login").into_response()), + }; - //let mut document = match ModelPermission::user_document(&ctx.kv_handles, user.id, document_id) { - // Ok(Some(document)) => document, - // Ok(None) => return Redirect::to("/documents").into_response(), - // Err(err) => { - // error!(?err, "failed to load document"); - // return internal_server_error(); - // } - //}; + let mut db = ctx.db_pool.get().map_err(internal_error)?; - //let new_document = Document { - // id: document.id, - // project_id: document.id, - // title: form.title.to_owned(), - // content: form.content.to_owned(), - //}; + let document_allowed = permissions::query::check_user_document( + &mut db, + &user.id, + &document_id.to_string(), + Permission::Write, + ).map_err(internal_error)?; - //if let Err(err) = new_document.save(&ctx.kv_handles) { - // error!(?err, "failed to save document"); - // return internal_server_error(); - //} - //info!(?new_document, "document updated"); + if !document_allowed { + return Err((StatusCode::FORBIDDEN, "permission denied".to_owned())); + } - //Redirect::to("/documents").into_response() + documents::query::update( + &mut db, + &document_id.to_string(), + form.title.to_owned(), + form.content.to_owned(), + ).map_err(internal_error)?; + + Ok(Redirect::to("/documents").into_response()) } diff --git a/src/kv.rs b/src/kv.rs deleted file mode 100644 index 0d68fa0..0000000 --- a/src/kv.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::path::Path; - -use anyhow::Result; -use fjall::{Config, Keyspace, PartitionCreateOptions, PartitionHandle}; - -/// Contains the handles needed to reference key-value data. -/// -/// This contains both the Keyspace and multiple PartitionHandle. -/// The Keyspace allows operational control and reporting at the top level, -/// while each PartitionHandle controls reading, writing, and removing from a -/// particular partition of the data. -/// -/// All fields are public, because this is meant to be used internally as a -/// wrapper to pass everything around, instead of passing each handle around by -/// itself. -#[derive(Clone)] -pub struct KvHandle { - pub keyspace: Keyspace, - - pub project: PartitionHandle, - pub document: PartitionHandle, - pub permissions: PartitionHandle, -} - -impl KvHandle { - pub fn open>(p: P) -> Result { - // TODO: those should probably be configurable, or like, not just hard coded. - let config = Config::new(p).flush_workers(4).compaction_workers(4); - let keyspace = Keyspace::open(config)?; - - let project = keyspace.open_partition("project", PartitionCreateOptions::default())?; - let document = keyspace.open_partition("document", PartitionCreateOptions::default())?; - let permissions = keyspace.open_partition("permissions", PartitionCreateOptions::default())?; - - Ok(KvHandle { - keyspace, - project, - document, - permissions, - }) - } -} diff --git a/src/lib.rs b/src/lib.rs index c6fb832..5705e0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,10 @@ pub mod handler; pub mod logging; pub mod models; pub mod password; +pub mod permissions; pub mod prelude; pub mod schema; pub mod serialize; pub mod server; pub mod session; pub mod templates; -pub mod kv; -pub mod permissions; diff --git a/src/models.rs b/src/models.rs index c83e2b3..3c3c7ab 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,7 +3,6 @@ use thiserror::Error; pub mod users; pub mod projects; pub mod types; -pub mod document_memberships; pub mod project_memberships; pub mod documents; diff --git a/src/models/document_memberships.rs b/src/models/document_memberships.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/models/documents.rs b/src/models/documents.rs index d891202..685130c 100644 --- a/src/models/documents.rs +++ b/src/models/documents.rs @@ -1,11 +1,12 @@ use diesel::prelude::*; +use serde::Serialize; use crate::schema::documents::dsl; use uuid::Uuid; use super::DbError; -#[derive(Queryable, Selectable, Debug, Clone)] +#[derive(Queryable, Selectable, Debug, Clone, Serialize)] #[diesel(table_name = crate::schema::documents)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Document { @@ -14,7 +15,9 @@ pub struct Document { pub project_id: String, pub title: String, pub content: String, + #[serde(skip)] pub created: chrono::NaiveDateTime, + #[serde(skip)] pub updated: chrono::NaiveDateTime, } @@ -29,7 +32,7 @@ pub struct NewDocument { } impl NewDocument { - pub fn new(creator_id: Uuid, project_id: Uuid, title: String, content: String) -> Self { + pub fn new(creator_id: &str, project_id: &str, title: String, content: String) -> Self { Self { id: Uuid::new_v4().to_string(), creator_id: creator_id.to_string(), @@ -40,19 +43,39 @@ impl NewDocument { } } -pub struct Query<'a> { - db: &'a mut SqliteConnection, -} +pub mod query { + use super::*; -impl<'a> Query<'a> { - pub fn new(db: &'a mut SqliteConnection) -> Self { - Self { db } + pub fn create(db: &mut SqliteConnection, new_document: NewDocument) -> Result { + diesel::insert_into(dsl::documents) + .values(&new_document) + .execute(db)?; + + let document = dsl::documents + .filter(dsl::id.eq(&new_document.id)) + .first(db)?; + + Ok(document) } - pub fn for_user(&mut self, user_id: String) -> Result, DbError> { - let documents = dsl::documents - .filter(dsl::creator_id.eq(user_id.to_string())) - .load::(self.db)?; - Ok(documents) + pub fn update(db: &mut SqliteConnection, document_id: &str, title: String, content: String) -> Result { + diesel::update(dsl::documents.filter(dsl::id.eq(document_id))) + .set((dsl::title.eq(title), dsl::content.eq(content))) + .execute(db)?; + + let document = dsl::documents + .filter(dsl::id.eq(document_id)) + .first(db)?; + + Ok(document) + } + + pub fn by_id(db: &mut SqliteConnection, document_id: &str) -> Result, DbError> { + let document = dsl::documents + .filter(dsl::id.eq(document_id)) + .first::(db) + .optional()?; + + Ok(document) } } diff --git a/src/permissions.rs b/src/permissions.rs index 7be551e..166019c 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -1,9 +1,64 @@ pub mod query { - use diesel::prelude::*; use crate::models::documents::Document; + use crate::models::project_memberships::ProjectRole; use crate::models::projects::Project; + use diesel::prelude::*; use diesel::SqliteConnection; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Permission { + Read, + Write, + Admin, + } + + pub fn check_user_project( + db: &mut SqliteConnection, + user_id: &str, + project_id: &str, + permission: Permission, + ) -> Result { + use crate::schema::project_memberships::dsl as pm; + + if permission == Permission::Admin { + let is_admin = pm::project_memberships + .filter(pm::user_id.eq(user_id)) + .filter(pm::project_id.eq(project_id)) + .filter(pm::role.eq(ProjectRole::Admin.to_string())) + .count() + .get_result::(db)?; + + Ok(is_admin > 0) + } else { + let is_member = pm::project_memberships + .filter(pm::user_id.eq(user_id)) + .filter(pm::project_id.eq(project_id)) + .count() + .get_result::(db)?; + + Ok(is_member > 0) + } + } + + pub fn check_user_document( + db: &mut SqliteConnection, + user_id: &str, + document_id: &str, + permission: Permission, + ) -> Result { + use crate::schema::documents::dsl as d; + + let document = d::documents + .filter(d::id.eq(document_id)) + .first::(db) + .optional()?; + + match document { + Some(doc) => check_user_project(db, user_id, &doc.project_id, permission), + None => Ok(false), + } + } + /// Users have permissions directly on projects which they are members of. pub fn accessible_project_ids( db: &mut SqliteConnection, @@ -57,7 +112,7 @@ pub mod query { let document_ids = direct_documents .into_iter() - .chain(project_documents.into_iter()) + .chain(project_documents) .collect(); Ok(document_ids) diff --git a/src/schema.rs b/src/schema.rs index 464807c..05c54f4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,16 +1,5 @@ // @generated automatically by Diesel CLI. -diesel::table! { - document_memberships (id) { - id -> Integer, - user_id -> Text, - document_id -> Text, - role -> Text, - created -> Timestamp, - updated -> Timestamp, - } -} - diesel::table! { documents (id) { id -> Text, @@ -59,7 +48,6 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - document_memberships, documents, project_memberships, projects, diff --git a/src/server.rs b/src/server.rs index b249c20..aec6cdd 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,16 +1,38 @@ use std::str::FromStr; use anyhow::Result; -use axum::{routing::{get, post}, Router}; +use axum::{ + routing::{get, post}, + Router, +}; use axum_login::AuthManagerLayerBuilder; use clap::Parser; use diesel_migrations::{embed_migrations, EmbeddedMigrations}; -use tower_http::{services::ServeDir, trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}}; +use tower_http::{ + services::ServeDir, + trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}, +}; use tower_sessions::SessionManagerLayer; use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore}; use tracing::Level; -use crate::{config::CommandLineOptions, context::Context, db, handler::{documents::{create_document_page, create_document_submit, documents_page, edit_document_page, edit_document_submit}, home::home_page, login::logout, login_page, login_submit, projects::{create_project_page, create_project_submit, projects_page}}, kv::KvHandle, logging::setup_logging, templates::make_template_loader}; +use crate::{ + config::CommandLineOptions, + context::Context, + db, + handler::{ + documents::{ + create_document_page, create_document_submit, documents_page, edit_document_page, + edit_document_submit, + }, + home::home_page, + login::logout, + login_page, login_submit, + projects::{create_project_page, create_project_submit, projects_page}, + }, + logging::setup_logging, + templates::make_template_loader, +}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/"); @@ -30,10 +52,7 @@ pub async fn run() -> Result<()> { let session_layer = create_session_manager_layer().await?; - // TODO: better name, also make it an option - let kv_handles = KvHandle::open("./kvdata/")?; - - let context = Context::new(db_pool, kv_handles, template_loader); + let context = Context::new(db_pool, template_loader); let auth_backend = context.clone(); let auth_layer = AuthManagerLayerBuilder::new(auth_backend, session_layer.clone()).build(); diff --git a/src/session.rs b/src/session.rs index 01e8b68..2587c46 100644 --- a/src/session.rs +++ b/src/session.rs @@ -47,7 +47,7 @@ impl AuthnBackend for Context { let mut db = self.db_pool.get()?; let mut q = users::Query::new(&mut db); - let user = q.by_id(&user_id)?; + let user = q.by_id(user_id)?; Ok(Some(user)) }