2025-03-21 06:50:31 +08:00
|
|
|
use anyhow::{Context, Result};
|
2025-03-12 05:09:19 +08:00
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
use database::{
|
2025-03-25 03:45:59 +08:00
|
|
|
client::SupabaseClient, enums::{AssetPermissionRole, AssetType, IdentityType}, models::AssetPermission, pool::get_pg_pool, schema::asset_permissions
|
2025-03-12 05:09:19 +08:00
|
|
|
};
|
|
|
|
use diesel::{prelude::*, upsert::excluded};
|
|
|
|
use diesel_async::RunQueryDsl;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
2025-03-19 23:41:29 +08:00
|
|
|
use crate::{errors::SharingError, user_lookup::find_user_by_email};
|
2025-03-19 23:35:56 +08:00
|
|
|
|
2025-03-12 05:09:19 +08:00
|
|
|
#[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<AssetPermission> {
|
|
|
|
let now = Utc::now();
|
|
|
|
|
|
|
|
// Validate asset type is not deprecated
|
|
|
|
if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) {
|
2025-03-19 23:41:29 +08:00
|
|
|
return Err(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)).into());
|
2025-03-12 05:09:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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::<Option<DateTime<Utc>>>(None),
|
|
|
|
))
|
|
|
|
.get_result(&mut conn)
|
|
|
|
.await
|
|
|
|
.context("Failed to create/update asset permission")
|
|
|
|
}
|
|
|
|
|
2025-03-19 23:35:56 +08:00
|
|
|
/// 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)
|
2025-03-19 23:48:56 +08:00
|
|
|
/// * `role` - The permission role to assign
|
2025-03-19 23:35:56 +08:00
|
|
|
/// * `created_by` - The ID of the user creating the permission
|
|
|
|
///
|
|
|
|
/// # Returns
|
|
|
|
/// * `Result<AssetPermission>` - 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<AssetPermission> {
|
|
|
|
// Validate asset type is not deprecated
|
|
|
|
if matches!(asset_type, AssetType::Dashboard | AssetType::Thread) {
|
2025-03-19 23:48:56 +08:00
|
|
|
return Err(SharingError::DeprecatedAssetType(format!("{:?}", asset_type)).into());
|
2025-03-19 23:35:56 +08:00
|
|
|
}
|
|
|
|
|
2025-03-19 23:41:29 +08:00
|
|
|
// Validate email format
|
|
|
|
if !email.contains('@') {
|
|
|
|
return Err(SharingError::InvalidEmail(email.to_string()).into());
|
2025-03-19 23:35:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Find the user by email
|
2025-03-25 03:45:59 +08:00
|
|
|
let user = match find_user_by_email(email).await? {
|
|
|
|
Some(user) => user,
|
|
|
|
None => {
|
|
|
|
// User doesn't exist, create a new user in Supabase Auth
|
|
|
|
// This will automatically create the user in the database through triggers
|
|
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let supabase_client = SupabaseClient::new()
|
|
|
|
.context("Failed to initialize Supabase client")?;
|
|
|
|
|
|
|
|
// Create user in Supabase Auth
|
|
|
|
supabase_client
|
|
|
|
.create_user(user_id, email)
|
|
|
|
.await
|
|
|
|
.context("Failed to create user in Supabase Auth")?;
|
|
|
|
|
|
|
|
// Wait for a moment to allow the database trigger to create the user
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
|
|
|
|
// Try to find the user again after creation
|
|
|
|
find_user_by_email(email).await?.ok_or_else(|| {
|
|
|
|
SharingError::UserNotFound(format!(
|
|
|
|
"User was created in Auth but not found in database: {}",
|
|
|
|
email
|
|
|
|
))
|
|
|
|
})?
|
|
|
|
}
|
|
|
|
};
|
2025-03-19 23:35:56 +08:00
|
|
|
|
2025-03-19 23:41:29 +08:00
|
|
|
// Create the share for the user
|
2025-03-19 23:35:56 +08:00
|
|
|
create_share(
|
|
|
|
asset_id,
|
|
|
|
asset_type,
|
|
|
|
user.id,
|
|
|
|
IdentityType::User,
|
|
|
|
role,
|
|
|
|
created_by,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2025-03-12 05:09:19 +08:00
|
|
|
/// Creates multiple sharing records in bulk
|
|
|
|
pub async fn create_shares_bulk(
|
|
|
|
shares: Vec<ShareCreationInput>,
|
|
|
|
created_by: Uuid,
|
|
|
|
) -> Result<Vec<AssetPermission>> {
|
|
|
|
let now = Utc::now();
|
|
|
|
|
|
|
|
// Validate no deprecated asset types
|
|
|
|
if shares
|
|
|
|
.iter()
|
|
|
|
.any(|s| matches!(s.asset_type, AssetType::Dashboard | AssetType::Thread))
|
|
|
|
{
|
2025-03-25 03:45:59 +08:00
|
|
|
return Err(SharingError::DeprecatedAssetType(
|
|
|
|
"Cannot create permissions for deprecated asset types".to_string(),
|
|
|
|
)
|
|
|
|
.into());
|
2025-03-12 05:09:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
let permissions: Vec<AssetPermission> = 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::<Option<DateTime<Utc>>>(None),
|
|
|
|
))
|
|
|
|
.get_results(&mut conn)
|
|
|
|
.await
|
|
|
|
.context("Failed to create/update asset permissions in bulk")
|
|
|
|
}
|
2025-03-19 23:35:56 +08:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2025-03-25 03:45:59 +08:00
|
|
|
|
2025-03-22 00:44:53 +08:00
|
|
|
use database::enums::{AssetPermissionRole, AssetType};
|
2025-03-19 23:41:29 +08:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_invalid_email_format() {
|
|
|
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let result = runtime.block_on(create_share_by_email(
|
|
|
|
"not-an-email",
|
2025-03-19 23:35:56 +08:00
|
|
|
Uuid::new_v4(),
|
2025-03-19 23:41:29 +08:00
|
|
|
AssetType::Collection,
|
|
|
|
AssetPermissionRole::Viewer,
|
2025-03-19 23:35:56 +08:00
|
|
|
Uuid::new_v4(),
|
2025-03-19 23:41:29 +08:00
|
|
|
));
|
2025-03-25 03:45:59 +08:00
|
|
|
|
2025-03-19 23:35:56 +08:00
|
|
|
assert!(result.is_err());
|
|
|
|
let err = result.unwrap_err().to_string();
|
2025-03-19 23:41:29 +08:00
|
|
|
assert!(err.contains("Invalid email"));
|
2025-03-19 23:35:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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();
|
2025-03-20 00:17:05 +08:00
|
|
|
assert!(err.contains("Deprecated asset type"));
|
|
|
|
assert!(err.contains("Dashboard"));
|
2025-03-19 23:35:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Note: Additional integration tests would be needed to test the database interactions
|
|
|
|
// These would require mocking the database or using a test database
|
|
|
|
}
|