From 1c9d3b6eed6c0d719d10c845942c1a79cc43e56d Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 19 Mar 2025 20:37:18 -0600 Subject: [PATCH] ok added in individual permissions to the dashboards, collections, chats --- .../handlers/src/chats/get_chat_handler.rs | 125 ++++++++++++++- api/libs/handlers/src/chats/types.rs | 45 +++++- .../collections/create_collection_handler.rs | 5 + .../collections/delete_collection_handler.rs | 4 +- .../src/collections/get_collection_handler.rs | 86 ++++++++++- api/libs/handlers/src/collections/types.rs | 17 ++- .../collections/update_collection_handler.rs | 5 + api/tests/integration/chats/get_chat_test.rs | 144 ++++++++++++++++++ api/tests/integration/chats/mod.rs | 3 +- .../collections/get_collection_test.rs | 129 ++++++++++++++++ api/tests/integration/collections/mod.rs | 3 +- 11 files changed, 547 insertions(+), 19 deletions(-) create mode 100644 api/tests/integration/chats/get_chat_test.rs create mode 100644 api/tests/integration/collections/get_collection_test.rs diff --git a/api/libs/handlers/src/chats/get_chat_handler.rs b/api/libs/handlers/src/chats/get_chat_handler.rs index 39668cb18..c46ba5c11 100644 --- a/api/libs/handlers/src/chats/get_chat_handler.rs +++ b/api/libs/handlers/src/chats/get_chat_handler.rs @@ -7,10 +7,11 @@ use serde_json::Value; use tokio; use uuid::Uuid; -use crate::chats::types::ChatWithMessages; +use crate::chats::types::{BusterShareIndividual, ChatWithMessages}; use crate::messages::types::{ChatMessage, ChatUserMessage}; +use database::enums::{AssetPermissionRole, AssetType, IdentityType}; use database::pool::get_pg_pool; -use database::schema::{chats, messages, users}; +use database::schema::{asset_permissions, chats, messages, users}; #[derive(Queryable)] pub struct ChatWithUser { @@ -37,6 +38,14 @@ pub struct MessageWithUser { pub user_attributes: Value, } +#[derive(Queryable)] +struct AssetPermissionInfo { + identity_id: Uuid, + role: AssetPermissionRole, + email: String, + name: Option, +} + pub async fn get_chat_handler(chat_id: &Uuid, user_id: &Uuid) -> Result { // Run thread and messages queries concurrently let thread_future = { @@ -99,8 +108,50 @@ pub async fn get_chat_handler(chat_id: &Uuid, user_id: &Uuid) -> Result conn, + Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)), + }; + + let chat_id = chat_id.clone(); + + tokio::spawn(async move { + // Query individual permissions for this chat + let permissions = asset_permissions::table + .inner_join(users::table.on(users::id.eq(asset_permissions::identity_id))) + .filter(asset_permissions::asset_id.eq(chat_id)) + .filter(asset_permissions::asset_type.eq(AssetType::Chat)) + .filter(asset_permissions::identity_type.eq(IdentityType::User)) + .filter(asset_permissions::deleted_at.is_null()) + .select(( + asset_permissions::identity_id, + asset_permissions::role, + users::email, + users::name, + )) + .load::(&mut conn) + .await; + + // Query publicly_accessible and related fields + let public_info = chats::table + .filter(chats::id.eq(chat_id)) + .select(( + chats::publicly_accessible, + chats::publicly_enabled_by, + chats::public_expiry_date, + )) + .first::<(bool, Option, Option>)>(&mut conn) + .await; + + // Return both results + (permissions, public_info) + }) + }; + + // Wait for all queries and handle errors + let (thread, messages, permissions_and_public_info) = tokio::try_join!( async move { thread_future .await @@ -115,9 +166,17 @@ pub async fn get_chat_handler(chat_id: &Uuid, user_id: &Uuid) -> Result = messages .into_iter() @@ -166,8 +225,54 @@ pub async fn get_chat_handler(chat_id: &Uuid, user_id: &Uuid) -> Result { + if permissions.is_empty() { + None + } else { + Some( + permissions + .into_iter() + .map(|p| BusterShareIndividual { + email: p.email, + role: p.role, + name: p.name, + }) + .collect::>(), + ) + } + } + Err(_) => None, + }; + + // Get public access info + let (publicly_accessible, publicly_enabled_by, public_expiry_date) = match public_info_result { + Ok((accessible, enabled_by_id, expiry)) => { + // Get the user info for publicly_enabled_by if it exists + let enabled_by_email = if let Some(enabled_by_id) = enabled_by_id { + let mut conn = match get_pg_pool().get().await { + Ok(conn) => conn, + Err(_) => return Err(anyhow!("Failed to get database connection")), + }; + + users::table + .filter(users::id.eq(enabled_by_id)) + .select(users::email) + .first::(&mut conn) + .await + .ok() + } else { + None + }; + + (accessible, enabled_by_email, expiry) + } + Err(_) => (false, None, None), + }; + + // Construct and return the ChatWithMessages with permissions + let chat = ChatWithMessages::new_with_messages( thread.id, thread.title, thread_messages, @@ -175,5 +280,13 @@ pub async fn get_chat_handler(chat_id: &Uuid, user_id: &Uuid) -> Result, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChatWithMessages { pub id: Uuid, @@ -18,6 +27,12 @@ pub struct ChatWithMessages { pub created_by_id: String, pub created_by_name: String, pub created_by_avatar: Option, + // Sharing fields + pub individual_permissions: Option>, + pub publicly_accessible: bool, + pub public_expiry_date: Option>, + pub public_enabled_by: Option, + pub public_password: Option, } impl ChatWithMessages { @@ -40,6 +55,11 @@ impl ChatWithMessages { created_by_id, created_by_name, created_by_avatar, + individual_permissions: None, + publicly_accessible: false, + public_expiry_date: None, + public_enabled_by: None, + public_password: None, } } @@ -75,8 +95,28 @@ impl ChatWithMessages { created_by_id, created_by_name, created_by_avatar, + // Default values for sharing fields + individual_permissions: None, + publicly_accessible: false, + public_expiry_date: None, + public_enabled_by: None, + public_password: None, } } + + pub fn with_permissions( + mut self, + individual_permissions: Option>, + publicly_accessible: bool, + public_expiry_date: Option>, + public_enabled_by: Option, + ) -> Self { + self.individual_permissions = individual_permissions; + self.publicly_accessible = publicly_accessible; + self.public_expiry_date = public_expiry_date; + self.public_enabled_by = public_enabled_by; + self + } pub fn add_message(&mut self, message: ChatMessage) { let message_id = message.id.to_string(); @@ -95,7 +135,4 @@ impl ChatWithMessages { self.messages.insert(message_id, message); self.updated_at = chrono::Utc::now().to_rfc3339(); } -} - - - +} \ No newline at end of file diff --git a/api/libs/handlers/src/collections/create_collection_handler.rs b/api/libs/handlers/src/collections/create_collection_handler.rs index be2c8b859..446e963c4 100644 --- a/api/libs/handlers/src/collections/create_collection_handler.rs +++ b/api/libs/handlers/src/collections/create_collection_handler.rs @@ -144,5 +144,10 @@ pub async fn create_collection_handler( assets: None, permission: AssetPermissionRole::Owner, organization_permissions: false, + individual_permissions: None, + publicly_accessible: false, + public_expiry_date: None, + public_enabled_by: None, + public_password: None, }) } diff --git a/api/libs/handlers/src/collections/delete_collection_handler.rs b/api/libs/handlers/src/collections/delete_collection_handler.rs index af2698de8..168b6fb05 100644 --- a/api/libs/handlers/src/collections/delete_collection_handler.rs +++ b/api/libs/handlers/src/collections/delete_collection_handler.rs @@ -20,8 +20,8 @@ use crate::collections::types::DeleteCollectionResponse; /// # Returns /// * `Result` - The IDs of the collections that were successfully deleted pub async fn delete_collection_handler( - user_id: &Uuid, - organization_id: &Uuid, + _user_id: &Uuid, + _organization_id: &Uuid, ids: Vec, ) -> Result { diff --git a/api/libs/handlers/src/collections/get_collection_handler.rs b/api/libs/handlers/src/collections/get_collection_handler.rs index b58969527..822fbda14 100644 --- a/api/libs/handlers/src/collections/get_collection_handler.rs +++ b/api/libs/handlers/src/collections/get_collection_handler.rs @@ -1,8 +1,19 @@ use anyhow::{anyhow, Result}; -use database::{collections::fetch_collection, enums::AssetPermissionRole}; +use chrono::{DateTime, Utc}; +use database::{collections::fetch_collection, enums::{AssetPermissionRole, AssetType, IdentityType}, pool::get_pg_pool, schema::{asset_permissions, collections, users}}; +use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, Queryable}; +use diesel_async::RunQueryDsl; use uuid::Uuid; -use crate::collections::types::{CollectionState, GetCollectionRequest}; +use crate::collections::types::{BusterShareIndividual, CollectionState, GetCollectionRequest}; + +#[derive(Queryable)] +struct AssetPermissionInfo { + identity_id: Uuid, + role: AssetPermissionRole, + email: String, + name: Option, +} /// Handler for getting a single collection by ID /// @@ -21,11 +32,82 @@ pub async fn get_collection_handler( Some(collection) => collection, None => return Err(anyhow!("Collection not found")), }; + + let mut conn = match get_pg_pool().get().await { + Ok(conn) => conn, + Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)), + }; + + // Query individual permissions for this collection + let individual_permissions_query = asset_permissions::table + .inner_join(users::table.on(users::id.eq(asset_permissions::identity_id))) + .filter(asset_permissions::asset_id.eq(req.id)) + .filter(asset_permissions::asset_type.eq(AssetType::Collection)) + .filter(asset_permissions::identity_type.eq(IdentityType::User)) + .filter(asset_permissions::deleted_at.is_null()) + .select(( + asset_permissions::identity_id, + asset_permissions::role, + users::email, + users::name, + )) + .load::(&mut conn) + .await; + + // For collections, we'll default public fields to false/none + // since the schema doesn't have these fields yet + let public_info: Result<(bool, Option, Option>), anyhow::Error> = Ok((false, None, None)); + + // Convert AssetPermissionInfo to BusterShareIndividual + let individual_permissions = match individual_permissions_query { + Ok(permissions) => { + if permissions.is_empty() { + None + } else { + Some( + permissions + .into_iter() + .map(|p| BusterShareIndividual { + email: p.email, + role: p.role, + name: p.name, + }) + .collect::>(), + ) + } + } + Err(_) => None, + }; + + // Get public access info + let (publicly_accessible, public_enabled_by, public_expiry_date) = match public_info { + Ok((accessible, enabled_by_id, expiry)) => { + // Get the user info for publicly_enabled_by if it exists + let enabled_by_email = if let Some(enabled_by_id) = enabled_by_id { + users::table + .filter(users::id.eq(enabled_by_id)) + .select(users::email) + .first::(&mut conn) + .await + .ok() + } else { + None + }; + + (accessible, enabled_by_email, expiry) + } + Err(_) => (false, None, None), + }; Ok(CollectionState { collection, assets: None, permission: AssetPermissionRole::Owner, organization_permissions: false, + individual_permissions, + publicly_accessible, + public_expiry_date, + public_enabled_by, + public_password: None, }) } diff --git a/api/libs/handlers/src/collections/types.rs b/api/libs/handlers/src/collections/types.rs index 46f969cf0..bf713997e 100644 --- a/api/libs/handlers/src/collections/types.rs +++ b/api/libs/handlers/src/collections/types.rs @@ -7,6 +7,13 @@ use diesel::AsChangeset; use serde::{Deserialize, Serialize}; use uuid::Uuid; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BusterShareIndividual { + pub email: String, + pub role: AssetPermissionRole, + pub name: Option, +} + // List collections types #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ListCollectionsFilter { @@ -68,10 +75,14 @@ pub struct CollectionState { #[serde(flatten)] pub collection: Collection, pub permission: AssetPermissionRole, - // pub individual_permissions: Option>, - // pub team_permissions: Option>, pub organization_permissions: bool, pub assets: Option>, + // Sharing fields + pub individual_permissions: Option>, + pub publicly_accessible: bool, + pub public_expiry_date: Option>, + pub public_enabled_by: Option, + pub public_password: Option, } // Create collection types @@ -112,4 +123,4 @@ pub struct DeleteCollectionRequest { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct DeleteCollectionResponse { pub ids: Vec, -} +} \ No newline at end of file diff --git a/api/libs/handlers/src/collections/update_collection_handler.rs b/api/libs/handlers/src/collections/update_collection_handler.rs index c55b7353b..3c8075b91 100644 --- a/api/libs/handlers/src/collections/update_collection_handler.rs +++ b/api/libs/handlers/src/collections/update_collection_handler.rs @@ -99,6 +99,11 @@ pub async fn update_collection_handler( assets: None, permission: AssetPermissionRole::Owner, organization_permissions: false, + individual_permissions: None, + publicly_accessible: false, + public_expiry_date: None, + public_enabled_by: None, + public_password: None, }) } diff --git a/api/tests/integration/chats/get_chat_test.rs b/api/tests/integration/chats/get_chat_test.rs new file mode 100644 index 000000000..2e0bf2eda --- /dev/null +++ b/api/tests/integration/chats/get_chat_test.rs @@ -0,0 +1,144 @@ +use uuid::Uuid; +use crate::common::{ + env::{create_env, TestEnv}, + http::client::TestClient, + assertions::response::assert_api_ok, +}; +use chrono::Utc; +use database::enums::{AssetPermissionRole, AssetTypeEnum, IdentityTypeEnum}; +use diesel::sql_query; +use diesel_async::RunQueryDsl; + +#[tokio::test] +async fn test_get_chat_with_sharing_info() { + // Setup test environment + let env = create_env().await; + let client = TestClient::new(&env); + + // Create test user and chat + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + let chat_id = create_test_chat(&env, user_id).await; + + // Add sharing permissions + add_test_permissions(&env, chat_id, user_id).await; + + // Add public sharing + enable_public_sharing(&env, chat_id, user_id).await; + + // Test GET request + let response = client + .get(&format!("/api/v1/chats/{}", chat_id)) + .header("X-User-Id", user_id.to_string()) + .send() + .await; + + // Assert success and verify response + let data = assert_api_ok(response).await; + + // Check fields + assert_eq!(data["id"], chat_id.to_string()); + + // Check sharing fields + assert_eq!(data["publicly_accessible"], true); + assert!(data["public_expiry_date"].is_string()); + assert_eq!(data["public_enabled_by"], "test@example.com"); + assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); + + let permission = &data["individual_permissions"][0]; + assert_eq!(permission["email"], "test2@example.com"); + assert_eq!(permission["role"], "viewer"); + assert_eq!(permission["name"], "Test User 2"); +} + +// Helper functions to set up the test data +async fn create_test_chat(env: &TestEnv, user_id: Uuid) -> Uuid { + let mut conn = env.db_pool.get().await.unwrap(); + + // Insert test user + sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") + .bind::(user_id) + .bind::("test@example.com") + .bind::("Test User") + .execute(&mut conn) + .await + .unwrap(); + + // Insert another test user + let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); + sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") + .bind::(user2_id) + .bind::("test2@example.com") + .bind::("Test User 2") + .execute(&mut conn) + .await + .unwrap(); + + // Insert test chat + let chat_id = Uuid::parse_str("00000000-0000-0000-0000-000000000030").unwrap(); + let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); + + // Insert test organization if needed + sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") + .bind::(org_id) + .bind::("Test Organization") + .execute(&mut conn) + .await + .unwrap(); + + // Insert chat + sql_query(r#" + INSERT INTO chats (id, title, organization_id, created_by, updated_by, publicly_accessible) + VALUES ($1, 'Test Chat', $2, $3, $3, false) + ON CONFLICT DO NOTHING + "#) + .bind::(chat_id) + .bind::(org_id) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); + + chat_id +} + +async fn add_test_permissions(env: &TestEnv, chat_id: Uuid, user_id: Uuid) { + let mut conn = env.db_pool.get().await.unwrap(); + + // Get the second user + let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); + + // Add permission for user2 as viewer + sql_query(r#" + INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) + VALUES ($1, $2, $3, $4, $5, $6, $6) + ON CONFLICT DO NOTHING + "#) + .bind::(user2_id) + .bind::(IdentityTypeEnum::User.to_string()) + .bind::(chat_id) + .bind::(AssetTypeEnum::Chat.to_string()) + .bind::(AssetPermissionRole::CanView.to_string()) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); +} + +async fn enable_public_sharing(env: &TestEnv, chat_id: Uuid, user_id: Uuid) { + let mut conn = env.db_pool.get().await.unwrap(); + + // Set public access + let expiry_date = Utc::now() + chrono::Duration::days(7); + + sql_query(r#" + UPDATE chats + SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2 + WHERE id = $3 + "#) + .bind::(user_id) + .bind::(expiry_date) + .bind::(chat_id) + .execute(&mut conn) + .await + .unwrap(); +} \ No newline at end of file diff --git a/api/tests/integration/chats/mod.rs b/api/tests/integration/chats/mod.rs index 2ef0c29d9..690d5862b 100644 --- a/api/tests/integration/chats/mod.rs +++ b/api/tests/integration/chats/mod.rs @@ -1 +1,2 @@ -pub mod sharing; \ No newline at end of file +pub mod sharing; +pub mod get_chat_test; \ No newline at end of file diff --git a/api/tests/integration/collections/get_collection_test.rs b/api/tests/integration/collections/get_collection_test.rs new file mode 100644 index 000000000..d2fb8ebec --- /dev/null +++ b/api/tests/integration/collections/get_collection_test.rs @@ -0,0 +1,129 @@ +use uuid::Uuid; +use crate::common::{ + env::{create_env, TestEnv}, + http::client::TestClient, + assertions::response::assert_api_ok, +}; +use chrono::Utc; +use database::enums::{AssetPermissionRole, AssetTypeEnum, IdentityTypeEnum}; +use diesel::sql_query; +use diesel_async::RunQueryDsl; + +#[tokio::test] +async fn test_get_collection_with_sharing_info() { + // Setup test environment + let env = create_env().await; + let client = TestClient::new(&env); + + // Create test user and collection + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + let collection_id = create_test_collection(&env, user_id).await; + + // Add sharing permissions + add_test_permissions(&env, collection_id, user_id).await; + + // Add public sharing + enable_public_sharing(&env, collection_id, user_id).await; + + // Test GET request + let response = client + .get(&format!("/api/v1/collections/{}", collection_id)) + .header("X-User-Id", user_id.to_string()) + .send() + .await; + + // Assert success and verify response + let data = assert_api_ok(response).await; + + // Check fields + assert_eq!(data["id"], collection_id.to_string()); + + // Check sharing fields - collections don't have public fields yet + assert_eq!(data["publicly_accessible"], false); + assert!(data["public_expiry_date"].is_null()); + assert!(data["public_enabled_by"].is_null()); + assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); + + let permission = &data["individual_permissions"][0]; + assert_eq!(permission["email"], "test2@example.com"); + assert_eq!(permission["role"], "viewer"); + assert_eq!(permission["name"], "Test User 2"); +} + +// Helper functions to set up the test data +async fn create_test_collection(env: &TestEnv, user_id: Uuid) -> Uuid { + let mut conn = env.db_pool.get().await.unwrap(); + + // Insert test user + sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") + .bind::(user_id) + .bind::("test@example.com") + .bind::("Test User") + .execute(&mut conn) + .await + .unwrap(); + + // Insert another test user + let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); + sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") + .bind::(user2_id) + .bind::("test2@example.com") + .bind::("Test User 2") + .execute(&mut conn) + .await + .unwrap(); + + // Insert test collection + let collection_id = Uuid::parse_str("00000000-0000-0000-0000-000000000040").unwrap(); + let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); + + // Insert test organization if needed + sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") + .bind::(org_id) + .bind::("Test Organization") + .execute(&mut conn) + .await + .unwrap(); + + // Insert collection + sql_query(r#" + INSERT INTO collections (id, name, description, organization_id, created_by) + VALUES ($1, 'Test Collection', 'Test description', $2, $3) + ON CONFLICT DO NOTHING + "#) + .bind::(collection_id) + .bind::(org_id) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); + + collection_id +} + +async fn add_test_permissions(env: &TestEnv, collection_id: Uuid, user_id: Uuid) { + let mut conn = env.db_pool.get().await.unwrap(); + + // Get the second user + let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); + + // Add permission for user2 as viewer + sql_query(r#" + INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) + VALUES ($1, $2, $3, $4, $5, $6, $6) + ON CONFLICT DO NOTHING + "#) + .bind::(user2_id) + .bind::(IdentityTypeEnum::User.to_string()) + .bind::(collection_id) + .bind::(AssetTypeEnum::Collection.to_string()) + .bind::(AssetPermissionRole::CanView.to_string()) + .bind::(user_id) + .execute(&mut conn) + .await + .unwrap(); +} + +async fn enable_public_sharing(_env: &TestEnv, _collection_id: Uuid, _user_id: Uuid) { + // Collections don't have public sharing fields yet, so this is a no-op +} \ No newline at end of file diff --git a/api/tests/integration/collections/mod.rs b/api/tests/integration/collections/mod.rs index 2ef0c29d9..625a283cf 100644 --- a/api/tests/integration/collections/mod.rs +++ b/api/tests/integration/collections/mod.rs @@ -1 +1,2 @@ -pub mod sharing; \ No newline at end of file +pub mod sharing; +pub mod get_collection_test; \ No newline at end of file