use anyhow::{Context, Result, anyhow}; use chrono::{DateTime, Utc}; use database::{ enums::{AssetPermissionRole, AssetType, IdentityType}, models::AssetPermission, pool::get_pg_pool, schema::asset_permissions, }; use diesel::{prelude::*, upsert::excluded}; use diesel_async::RunQueryDsl; use uuid::Uuid; use crate::errors::SharingError; use crate::types::find_user_by_email; #[derive(Debug)] pub struct ShareCreationInput { pub asset_id: Uuid, pub asset_type: AssetType, pub identity_id: Uuid, pub identity_type: IdentityType, pub role: AssetPermissionRole, } /// Creates a new sharing record for an asset pub async fn create_share( asset_id: Uuid, asset_type: AssetType, identity_id: Uuid, identity_type: IdentityType, role: AssetPermissionRole, created_by: Uuid, ) -> Result { let now = Utc::now(); // Validate asset type is not deprecated if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) { anyhow::bail!("Asset type {:?} is deprecated", asset_type); } let mut conn = get_pg_pool().get().await?; let permission = AssetPermission { identity_id, identity_type, asset_id, asset_type, role, created_at: now, updated_at: now, deleted_at: None, created_by, updated_by: created_by, }; diesel::insert_into(asset_permissions::table) .values(&permission) .on_conflict(( asset_permissions::identity_id, asset_permissions::asset_id, asset_permissions::asset_type, asset_permissions::identity_type, )) .do_update() .set(( asset_permissions::role.eq(role), asset_permissions::updated_at.eq(now), asset_permissions::updated_by.eq(created_by), asset_permissions::deleted_at.eq::>>(None), )) .get_result(&mut conn) .await .context("Failed to create/update asset permission") } /// Creates or updates an asset permission for a user identified by email /// /// # Arguments /// * `email` - The email address of the user to grant access to /// * `asset_id` - The ID of the asset to share /// * `asset_type` - The type of asset (must not be deprecated) /// * `role` - The permission role to assign (must be Owner or FullAccess) /// * `created_by` - The ID of the user creating the permission /// /// # Returns /// * `Result` - The created or updated permission record pub async fn create_share_by_email( email: &str, asset_id: Uuid, asset_type: AssetType, role: AssetPermissionRole, created_by: Uuid, ) -> Result { // Validate asset type is not deprecated if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) { return Err(anyhow!(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)))); } // Validate role is either Owner or FullAccess if !matches!(role, AssetPermissionRole::Owner | AssetPermissionRole::FullAccess) { return Err(anyhow!(SharingError::InvalidPermissionRole(format!( "Role must be Owner or FullAccess, got {:?}", role )))); } // Find the user by email let user = find_user_by_email(email).await? .ok_or_else(|| anyhow!(SharingError::UserNotFound(email.to_string())))?; // Create or update the permission create_share( asset_id, asset_type, user.id, IdentityType::User, role, created_by, ) .await } /// Creates multiple sharing records in bulk pub async fn create_shares_bulk( shares: Vec, created_by: Uuid, ) -> Result> { let now = Utc::now(); // Validate no deprecated asset types if shares .iter() .any(|s| matches!(s.asset_type, AssetType::Dashboard | AssetType::Thread)) { anyhow::bail!("Cannot create permissions for deprecated asset types"); } let permissions: Vec = shares .into_iter() .map(|share| AssetPermission { identity_id: share.identity_id, identity_type: share.identity_type, asset_id: share.asset_id, asset_type: share.asset_type, role: share.role, created_at: now, updated_at: now, deleted_at: None, created_by, updated_by: created_by, }) .collect(); let mut conn = get_pg_pool().get().await?; diesel::insert_into(asset_permissions::table) .values(&permissions) .on_conflict(( asset_permissions::identity_id, asset_permissions::asset_id, asset_permissions::asset_type, asset_permissions::identity_type, )) .do_update() .set(( asset_permissions::role.eq(excluded(asset_permissions::role)), asset_permissions::updated_at.eq(now), asset_permissions::updated_by.eq(created_by), asset_permissions::deleted_at.eq::>>(None), )) .get_results(&mut conn) .await .context("Failed to create/update asset permissions in bulk") } #[cfg(test)] mod tests { use super::*; use crate::errors::SharingError; #[tokio::test] async fn test_create_share_by_email_validates_role() { // Test that only Owner and FullAccess roles are accepted let result = create_share_by_email( "test@example.com", Uuid::new_v4(), AssetType::Chat, AssetPermissionRole::CanEdit, Uuid::new_v4(), ) .await; assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("Role must be Owner or FullAccess")); } #[tokio::test] async fn test_create_share_by_email_validates_asset_type() { // Test that deprecated asset types are rejected let result = create_share_by_email( "test@example.com", Uuid::new_v4(), AssetType::Dashboard, // Deprecated asset type AssetPermissionRole::Owner, Uuid::new_v4(), ) .await; assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("Asset type")); assert!(err.contains("is deprecated")); } // Note: Additional integration tests would be needed to test the database interactions // These would require mocking the database or using a test database }