diff --git a/.sqlx/query-e9e22adc5a6f6daf354dc122cadab41b7cc13e0d956b3204d22f26a47ad594e3.json b/.sqlx/query-e9e22adc5a6f6daf354dc122cadab41b7cc13e0d956b3204d22f26a47ad594e3.json new file mode 100644 index 00000000..0139ec17 --- /dev/null +++ b/.sqlx/query-e9e22adc5a6f6daf354dc122cadab41b7cc13e0d956b3204d22f26a47ad594e3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n af.uuid\n FROM af_published_collab apc\n JOIN af_user af ON af.uid = apc.published_by\n WHERE view_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "uuid", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e9e22adc5a6f6daf354dc122cadab41b7cc13e0d956b3204d22f26a47ad594e3" +} diff --git a/libs/authentication/src/jwt.rs b/libs/authentication/src/jwt.rs index 1a6da51a..355ba6c8 100644 --- a/libs/authentication/src/jwt.rs +++ b/libs/authentication/src/jwt.rs @@ -58,6 +58,33 @@ impl FromRequest for UserUuid { } } +// For cases where the handler itself will handle the request differently +// based on whether the user is authenticated or not +pub struct OptionalUserUuid(Option); + +impl FromRequest for OptionalUserUuid { + type Error = actix_web::Error; + + type Future = std::future::Ready>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let auth = get_auth_from_request(req); + match auth { + Ok(auth) => match UserUuid::from_auth(auth) { + Ok(uuid) => std::future::ready(Ok(OptionalUserUuid(Some(uuid)))), + Err(_) => std::future::ready(Ok(OptionalUserUuid(None))), + }, + Err(_) => std::future::ready(Ok(OptionalUserUuid(None))), + } + } +} + +impl OptionalUserUuid { + pub fn as_uuid(&self) -> Option { + self.0.as_deref().map(|uuid| uuid.to_owned()) + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct Authorization { pub token: String, diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 4f8c8316..638abfaa 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -843,6 +843,16 @@ impl Client { Ok(()) } + #[instrument(level = "debug", skip_all, err)] + pub async fn http_client_without_auth( + &self, + method: Method, + url: &str, + ) -> Result { + trace!("start request: {}, method: {}", url, method,); + Ok(self.cloud_client.request(method, url)) + } + #[instrument(level = "debug", skip_all, err)] pub async fn http_client_with_auth( &self, diff --git a/libs/client-api/src/http_publish.rs b/libs/client-api/src/http_publish.rs index 389a9f69..334a5e4f 100644 --- a/libs/client-api/src/http_publish.rs +++ b/libs/client-api/src/http_publish.rs @@ -153,6 +153,29 @@ impl Client { } } +// Optional login +impl Client { + pub async fn get_published_view_comments( + &self, + view_id: &uuid::Uuid, + ) -> Result { + let url = format!( + "{}/api/workspace/published-info/{}/comment", + self.base_url, view_id + ); + let client = if let Ok(client) = self.http_client_with_auth(Method::GET, &url).await { + client + } else { + self.http_client_without_auth(Method::GET, &url).await? + }; + + let resp = client.send().await?; + AppResponse::::from_response(resp) + .await? + .into_data() + } +} + // Guest API (no login required) impl Client { #[instrument(level = "debug", skip_all)] @@ -236,20 +259,6 @@ impl Client { Ok(bytes) } - pub async fn get_published_view_comments( - &self, - view_id: &uuid::Uuid, - ) -> Result { - let url = format!( - "{}/api/workspace/published-info/{}/comment", - self.base_url, view_id - ); - let resp = self.cloud_client.get(&url).send().await?; - AppResponse::::from_response(resp) - .await? - .into_data() - } - pub async fn get_published_view_reactions( &self, view_id: &uuid::Uuid, diff --git a/libs/database-entity/src/dto.rs b/libs/database-entity/src/dto.rs index d155d9ee..18ab6ec7 100644 --- a/libs/database-entity/src/dto.rs +++ b/libs/database-entity/src/dto.rs @@ -865,6 +865,7 @@ pub struct GlobalComment { pub reply_comment_id: Option, pub comment_id: Uuid, pub is_deleted: bool, + pub can_be_deleted: bool, } #[derive(Serialize, Deserialize, Debug)] diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 2e1b0123..01e78140 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -1119,12 +1119,34 @@ pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgre Ok(res) } -pub async fn select_comments_for_published_view_orderd_by_recency< +pub async fn select_owner_of_published_collab<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + view_id: &Uuid, +) -> Result { + let res = sqlx::query!( + r#" + SELECT + af.uuid + FROM af_published_collab apc + JOIN af_user af ON af.uid = apc.published_by + WHERE view_id = $1 + "#, + view_id, + ) + .fetch_one(executor) + .await?; + + Ok(res.uuid) +} + +pub async fn select_comments_for_published_view_ordered_by_recency< 'a, E: Executor<'a, Database = Postgres>, >( executor: E, view_id: &Uuid, + user_uuid: &Option, + page_owner_uuid: &Uuid, ) -> Result, AppError> { let rows = sqlx::query!( r#" @@ -1158,6 +1180,8 @@ pub async fn select_comments_for_published_view_orderd_by_recency< .unwrap_or("".to_string()), avatar_url: None, }); + let is_page_owner = user_uuid.as_ref() == Some(page_owner_uuid); + let is_comment_creator = user_uuid.as_ref() == comment_creator.as_ref().map(|u| &u.uuid); GlobalComment { user: comment_creator, comment_id: row.comment_id, @@ -1166,6 +1190,7 @@ pub async fn select_comments_for_published_view_orderd_by_recency< content: row.content.clone(), reply_comment_id: row.reply_comment_id, is_deleted: row.is_deleted, + can_be_deleted: !row.is_deleted && (is_page_owner || is_comment_creator), } }) .collect(); diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 829f13d2..dde1872d 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -25,7 +25,7 @@ use access_control::collab::CollabAccessControl; use app_error::AppError; use appflowy_collaborate::actix_ws::entities::ClientStreamMessage; use appflowy_collaborate::indexer::IndexerProvider; -use authentication::jwt::UserUuid; +use authentication::jwt::{OptionalUserUuid, UserUuid}; use collab_rt_entity::realtime_proto::HttpRealtimeMessage; use collab_rt_entity::RealtimeMessage; use collab_rt_protocol::validate_encode_collab; @@ -1097,10 +1097,12 @@ async fn get_published_collab_info_handler( async fn get_published_collab_comment_handler( view_id: web::Path, + optional_user_uuid: OptionalUserUuid, state: Data, ) -> Result> { let view_id = view_id.into_inner(); - let comments = get_comments_on_published_view(&state.pg_pool, &view_id).await?; + let comments = + get_comments_on_published_view(&state.pg_pool, &view_id, &optional_user_uuid).await?; let resp = GlobalComments { comments }; Ok(Json(AppResponse::Ok().with_data(resp))) } diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 504e7a9e..5a0a9cfd 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -1,3 +1,4 @@ +use authentication::jwt::OptionalUserUuid; use database_entity::dto::{AFWorkspaceSettingsChange, PublishCollabItem}; use std::collections::HashMap; @@ -165,8 +166,16 @@ pub async fn get_published_collab_info( pub async fn get_comments_on_published_view( pg_pool: &PgPool, view_id: &Uuid, + optional_user_uuid: &OptionalUserUuid, ) -> Result, AppError> { - let comments = select_comments_for_published_view_orderd_by_recency(pg_pool, view_id).await?; + let page_owner_uuid = select_owner_of_published_collab(pg_pool, view_id).await?; + let comments = select_comments_for_published_view_ordered_by_recency( + pg_pool, + view_id, + &optional_user_uuid.as_uuid(), + &page_owner_uuid, + ) + .await?; Ok(comments) } diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index b1647c8d..beffa4ea 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -267,26 +267,35 @@ async fn test_publish_comments() { assert!(result.is_err()); assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn); - // Test if only all users, authenticated or not, can view all the comments + // Test if only all users, authenticated or not, can view all the comments, + // and whether the `can_be_deleted` field is correctly set let published_view_comments: Vec = page_owner_client .get_published_view_comments(&view_id) .await .unwrap() .comments; assert_eq!(published_view_comments.len(), 2); + assert!(published_view_comments.iter().all(|c| c.can_be_deleted)); let published_view_comments: Vec = first_user_client .get_published_view_comments(&view_id) .await .unwrap() .comments; assert_eq!(published_view_comments.len(), 2); + assert_eq!( + published_view_comments + .iter() + .map(|c| c.can_be_deleted) + .collect_vec(), + vec![true, false] + ); let published_view_comments: Vec = guest_client .get_published_view_comments(&view_id) .await .unwrap() .comments; assert_eq!(published_view_comments.len(), 2); - assert!(published_view_comments.iter().all(|c| !c.is_deleted)); + assert!(published_view_comments.iter().all(|c| !c.can_be_deleted)); // Test if the comments are correctly sorted let comment_creators = published_view_comments @@ -401,6 +410,7 @@ async fn test_publish_comments() { .comments; assert_eq!(published_view_comments.len(), 3); assert!(published_view_comments.iter().all(|c| c.is_deleted)); + assert!(published_view_comments.iter().all(|c| !c.can_be_deleted)); } #[tokio::test]