feat: add can be deleted field to GlobalComment

This commit is contained in:
Khor Shu Heng 2024-07-26 22:50:55 +08:00 committed by khorshuheng
parent 928781d01e
commit ff7de66dfa
9 changed files with 135 additions and 20 deletions

View file

@ -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"
}

View file

@ -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<UserUuid>);
impl FromRequest for OptionalUserUuid {
type Error = actix_web::Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
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<uuid::Uuid> {
self.0.as_deref().map(|uuid| uuid.to_owned())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Authorization {
pub token: String,

View file

@ -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<RequestBuilder, AppResponseError> {
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,

View file

@ -153,6 +153,29 @@ impl Client {
}
}
// Optional login
impl Client {
pub async fn get_published_view_comments(
&self,
view_id: &uuid::Uuid,
) -> Result<GlobalComments, AppResponseError> {
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::<GlobalComments>::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<GlobalComments, AppResponseError> {
let url = format!(
"{}/api/workspace/published-info/{}/comment",
self.base_url, view_id
);
let resp = self.cloud_client.get(&url).send().await?;
AppResponse::<GlobalComments>::from_response(resp)
.await?
.into_data()
}
pub async fn get_published_view_reactions(
&self,
view_id: &uuid::Uuid,

View file

@ -865,6 +865,7 @@ pub struct GlobalComment {
pub reply_comment_id: Option<Uuid>,
pub comment_id: Uuid,
pub is_deleted: bool,
pub can_be_deleted: bool,
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -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<Uuid, AppError> {
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<Uuid>,
page_owner_uuid: &Uuid,
) -> Result<Vec<GlobalComment>, 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();

View file

@ -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<Uuid>,
optional_user_uuid: OptionalUserUuid,
state: Data<AppState>,
) -> Result<JsonAppResponse<GlobalComments>> {
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)))
}

View file

@ -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<Vec<GlobalComment>, 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)
}

View file

@ -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<GlobalComment> = 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<GlobalComment> = 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<GlobalComment> = 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]